/** * 🧠 Pattern Library for Natural Language Processing * Manages pre-computed pattern embeddings and smart matching * * Uses Brainy's own features for self-leveraging intelligence: * - Embeddings for semantic similarity * - Pattern caching for performance * - Progressive learning from usage */ import { Vector } from '../coreTypes.js' import { Brainy } from '../brainy.js' import { EMBEDDED_PATTERNS, getPatternEmbeddings, PATTERNS_METADATA } from './embeddedPatterns.js' export interface Pattern { id: string category: string examples: string[] pattern: string template: any confidence: number embedding?: Vector domain?: string frequency?: number | string slots?: SlotDefinition[] // Named slot definitions } export interface SlotDefinition { name: string type: 'text' | 'number' | 'date' | 'entity' | 'location' | 'person' | 'any' required?: boolean default?: any pattern?: string // Optional regex for validation transform?: (value: string) => any // Optional transformation function } export interface SlotExtraction { slots: Record confidence: number errors?: string[] // Validation errors } export class PatternLibrary { private patterns: Map private patternEmbeddings: Map private brain: Brainy private embeddingCache: Map private successMetrics: Map constructor(brain: Brainy) { this.brain = brain this.patterns = new Map() this.patternEmbeddings = new Map() this.embeddingCache = new Map() this.successMetrics = new Map() } /** * Initialize pattern library with pre-computed embeddings */ async init(): Promise { // Try to load pre-computed embeddings first const precomputedEmbeddings = getPatternEmbeddings() if (precomputedEmbeddings.size > 0) { // Use pre-computed embeddings (instant!) console.debug(`Loading ${precomputedEmbeddings.size} pre-computed pattern embeddings`) for (const pattern of EMBEDDED_PATTERNS) { this.patterns.set(pattern.id, pattern) this.successMetrics.set(pattern.id, pattern.confidence) const embedding = precomputedEmbeddings.get(pattern.id) if (embedding) { this.patternEmbeddings.set(pattern.id, Array.from(embedding)) } } console.debug(`Pattern library ready: ${PATTERNS_METADATA.totalPatterns} patterns loaded instantly`) } else { // Fall back to runtime computation console.debug('No pre-computed embeddings found, computing at runtime...') for (const pattern of EMBEDDED_PATTERNS) { this.patterns.set(pattern.id, pattern) this.successMetrics.set(pattern.id, pattern.confidence) } // Compute embeddings for all patterns await this.precomputeEmbeddings() } } /** * Pre-compute embeddings for all patterns for fast matching */ private async precomputeEmbeddings(): Promise { for (const [id, pattern] of this.patterns) { // Average embeddings of all examples for robust representation const embeddings: Vector[] = [] for (const example of pattern.examples) { const embedding = await this.getEmbedding(example) embeddings.push(embedding) } // Average the embeddings const avgEmbedding = this.averageVectors(embeddings) this.patternEmbeddings.set(id, avgEmbedding) } } /** * Get embedding with caching */ private async getEmbedding(text: string): Promise { if (this.embeddingCache.has(text)) { return this.embeddingCache.get(text)! } // Use brain's embed method directly to avoid recursion const embedding = await (this.brain as any).embed(text) this.embeddingCache.set(text, embedding) return embedding } /** * Find best matching patterns for a query */ async findBestPatterns(queryEmbedding: Vector, k: number = 3): Promise> { const matches: Array<{ pattern: Pattern; similarity: number }> = [] // Calculate similarity with all patterns for (const [id, patternEmbedding] of this.patternEmbeddings) { const similarity = this.cosineSimilarity(queryEmbedding, patternEmbedding) const pattern = this.patterns.get(id)! // Apply success metric boost const successBoost = this.successMetrics.get(id) || 0.5 const adjustedSimilarity = similarity * (0.7 + 0.3 * successBoost) matches.push({ pattern, similarity: adjustedSimilarity }) } // Sort by similarity and return top k matches.sort((a, b) => b.similarity - a.similarity) return matches.slice(0, k) } /** * Extract slots from query based on pattern with enhanced fuzzy matching */ extractSlots(query: string, pattern: Pattern): SlotExtraction { const slots: Record = {} const errors: string[] = [] let confidence = pattern.confidence // If pattern has named slot definitions, use them if (pattern.slots && pattern.slots.length > 0) { return this.extractNamedSlots(query, pattern) } // Try regex extraction first const regex = new RegExp(pattern.pattern, 'i') const match = query.match(regex) if (match) { // Extract captured groups as slots for (let i = 1; i < match.length; i++) { slots[`$${i}`] = match[i] } // High confidence if regex matches confidence = Math.min(confidence * 1.2, 1.0) } else { // Enhanced fuzzy matching with Levenshtein distance const fuzzyResult = this.fuzzyExtractSlots(query, pattern) Object.assign(slots, fuzzyResult.slots) confidence = fuzzyResult.confidence if (fuzzyResult.errors) { errors.push(...fuzzyResult.errors) } } // Post-process slots this.postProcessSlots(slots, pattern) return { slots, confidence, errors: errors.length > 0 ? errors : undefined } } /** * Extract named slots with type validation */ private extractNamedSlots(query: string, pattern: Pattern): SlotExtraction { const slots: Record = {} const errors: string[] = [] let confidence = pattern.confidence if (!pattern.slots) { return { slots, confidence } } // Create a flexible regex from pattern let flexiblePattern = pattern.pattern const slotPositions: Map = new Map() // Replace named slots in pattern with capture groups pattern.slots.forEach((slot, index) => { const slotPattern = slot.pattern || this.getDefaultPatternForType(slot.type) flexiblePattern = flexiblePattern.replace( new RegExp(`\\{${slot.name}\\}`, 'g'), `(${slotPattern})` ) slotPositions.set(index + 1, slot) }) const regex = new RegExp(flexiblePattern, 'i') const match = query.match(regex) if (match) { // Extract and validate each slot slotPositions.forEach((slotDef, position) => { const value = match[position] if (value) { // Apply transformation if defined const transformedValue = slotDef.transform ? slotDef.transform(value) : this.transformByType(value, slotDef.type) // Validate the value if (this.validateSlotValue(transformedValue, slotDef)) { slots[slotDef.name] = transformedValue } else { errors.push(`Invalid value for slot '${slotDef.name}': expected ${slotDef.type}, got '${value}'`) confidence *= 0.8 } } else if (slotDef.required) { if (slotDef.default !== undefined) { slots[slotDef.name] = slotDef.default } else { errors.push(`Required slot '${slotDef.name}' not found`) confidence *= 0.5 } } }) } else { // Try fuzzy matching for named slots const fuzzyResult = this.fuzzyExtractNamedSlots(query, pattern) Object.assign(slots, fuzzyResult.slots) confidence = fuzzyResult.confidence if (fuzzyResult.errors) { errors.push(...fuzzyResult.errors) } } return { slots, confidence, errors: errors.length > 0 ? errors : undefined } } /** * Fuzzy extraction using Levenshtein distance */ private fuzzyExtractSlots(query: string, pattern: Pattern): SlotExtraction { const slots: Record = {} let bestConfidence = 0 // Try each example with fuzzy matching for (const example of pattern.examples) { const distance = this.levenshteinDistance(query.toLowerCase(), example.toLowerCase()) const similarity = 1 - (distance / Math.max(query.length, example.length)) if (similarity > 0.6) { // 60% similarity threshold // Extract slots using alignment const aligned = this.alignStrings(query, example) const extractedSlots = this.extractSlotsFromAlignment(aligned, pattern) if (Object.keys(extractedSlots).length > 0) { const currentConfidence = pattern.confidence * similarity if (currentConfidence > bestConfidence) { Object.assign(slots, extractedSlots) bestConfidence = currentConfidence } } } } return { slots, confidence: bestConfidence, errors: bestConfidence < 0.5 ? ['Low confidence fuzzy match'] : undefined } } /** * Fuzzy extraction for named slots */ private fuzzyExtractNamedSlots(query: string, pattern: Pattern): SlotExtraction { const slots: Record = {} const errors: string[] = [] let confidence = pattern.confidence * 0.7 // Lower confidence for fuzzy if (!pattern.slots) { return { slots, confidence } } // Tokenize query for flexible matching const tokens = this.tokenize(query) pattern.slots.forEach(slotDef => { const value = this.findSlotValueInTokens(tokens, slotDef) if (value) { const transformedValue = slotDef.transform ? slotDef.transform(value) : this.transformByType(value, slotDef.type) if (this.validateSlotValue(transformedValue, slotDef)) { slots[slotDef.name] = transformedValue } else { errors.push(`Fuzzy match: uncertain value for '${slotDef.name}'`) confidence *= 0.9 } } else if (slotDef.required && slotDef.default !== undefined) { slots[slotDef.name] = slotDef.default } }) return { slots, confidence, errors: errors.length > 0 ? errors : undefined } } /** * Find slot value in tokens based on type */ private findSlotValueInTokens(tokens: string[], slotDef: SlotDefinition): string | null { const joinedTokens = tokens.join(' ') switch (slotDef.type) { case 'number': const numberMatch = joinedTokens.match(/\d+(\.\d+)?/) return numberMatch ? numberMatch[0] : null case 'date': const datePatterns = [ /\d{4}-\d{2}-\d{2}/, /\d{1,2}\/\d{1,2}\/\d{2,4}/, /(january|february|march|april|may|june|july|august|september|october|november|december)\s+\d{1,2},?\s+\d{4}/i, /(today|tomorrow|yesterday)/i ] for (const pattern of datePatterns) { const match = joinedTokens.match(pattern) if (match) return match[0] } return null case 'person': // Look for capitalized words (proper nouns) const personMatch = joinedTokens.match(/\b[A-Z][a-z]+(\s+[A-Z][a-z]+)*\b/) return personMatch ? personMatch[0] : null case 'location': // Look for location indicators const locationPatterns = [ /\b(in|at|from|to)\s+([A-Z][a-z]+(\s+[A-Z][a-z]+)*)\b/, /\b[A-Z][a-z]+,\s+[A-Z]{2}\b/ // City, STATE format ] for (const pattern of locationPatterns) { const match = joinedTokens.match(pattern) if (match) return match[2] || match[0] } return null case 'entity': case 'text': case 'any': default: // Return first non-common word as potential value const commonWords = new Set(['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for']) const significantToken = tokens.find(t => !commonWords.has(t.toLowerCase())) return significantToken || null } } /** * Get default regex pattern for slot type */ private getDefaultPatternForType(type: string): string { switch (type) { case 'number': return '\\d+(?:\\.\\d+)?' case 'date': return '[\\w\\s,/-]+' case 'person': return '[A-Z][a-z]+(?:\\s+[A-Z][a-z]+)*' case 'location': return '[A-Z][a-z]+(?:[\\s,]+[A-Z][a-z]+)*' case 'entity': return '[\\w\\s-]+' case 'text': case 'any': default: return '.+' } } /** * Transform value based on type */ private transformByType(value: string, type: string): any { switch (type) { case 'number': const num = parseFloat(value) return isNaN(num) ? value : num case 'date': // Simple date parsing if (value.toLowerCase() === 'today') { return new Date().toISOString().split('T')[0] } else if (value.toLowerCase() === 'tomorrow') { const tomorrow = new Date() tomorrow.setDate(tomorrow.getDate() + 1) return tomorrow.toISOString().split('T')[0] } else if (value.toLowerCase() === 'yesterday') { const yesterday = new Date() yesterday.setDate(yesterday.getDate() - 1) return yesterday.toISOString().split('T')[0] } return value case 'person': case 'location': case 'entity': // Capitalize properly return value.split(' ') .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(' ') default: return value.trim() } } /** * Validate slot value against definition */ private validateSlotValue(value: any, slotDef: SlotDefinition): boolean { if (value === null || value === undefined) { return !slotDef.required } switch (slotDef.type) { case 'number': return typeof value === 'number' && !isNaN(value) case 'date': return typeof value === 'string' && value.length > 0 case 'text': case 'person': case 'location': case 'entity': return typeof value === 'string' && value.length > 0 case 'any': return true default: return true } } /** * Calculate Levenshtein distance between two strings */ private levenshteinDistance(s1: string, s2: string): number { const len1 = s1.length const len2 = s2.length const matrix: number[][] = [] for (let i = 0; i <= len1; i++) { matrix[i] = [i] } for (let j = 0; j <= len2; j++) { matrix[0][j] = j } for (let i = 1; i <= len1; i++) { for (let j = 1; j <= len2; j++) { const cost = s1[i - 1] === s2[j - 1] ? 0 : 1 matrix[i][j] = Math.min( matrix[i - 1][j] + 1, // deletion matrix[i][j - 1] + 1, // insertion matrix[i - 1][j - 1] + cost // substitution ) } } return matrix[len1][len2] } /** * Align two strings for slot extraction */ private alignStrings(query: string, example: string): Array<[string, string]> { const queryTokens = this.tokenize(query) const exampleTokens = this.tokenize(example) const aligned: Array<[string, string]> = [] let i = 0, j = 0 while (i < queryTokens.length && j < exampleTokens.length) { if (queryTokens[i] === exampleTokens[j]) { aligned.push([queryTokens[i], exampleTokens[j]]) i++ j++ } else { // Try to find best match const bestMatch = this.findBestTokenMatch(queryTokens[i], exampleTokens.slice(j, j + 3)) if (bestMatch.index >= 0) { j += bestMatch.index aligned.push([queryTokens[i], exampleTokens[j]]) } else { aligned.push([queryTokens[i], exampleTokens[j]]) } i++ j++ } } return aligned } /** * Find best token match using fuzzy comparison */ private findBestTokenMatch(token: string, candidates: string[]): { index: number; similarity: number } { let bestIndex = -1 let bestSimilarity = 0 candidates.forEach((candidate, index) => { const distance = this.levenshteinDistance(token.toLowerCase(), candidate.toLowerCase()) const similarity = 1 - (distance / Math.max(token.length, candidate.length)) if (similarity > bestSimilarity && similarity > 0.6) { bestIndex = index bestSimilarity = similarity } }) return { index: bestIndex, similarity: bestSimilarity } } /** * Extract slots from string alignment */ private extractSlotsFromAlignment(aligned: Array<[string, string]>, _pattern: Pattern): Record { const slots: Record = {} let slotIndex = 1 aligned.forEach(([queryToken, exampleToken]) => { if (exampleToken.startsWith('$')) { slots[`$${slotIndex}`] = queryToken slotIndex++ } }) return slots } /** * Fill template with extracted slots */ fillTemplate(template: any, slots: Record): any { const filled = JSON.parse(JSON.stringify(template)) // Recursively replace slot placeholders const replacePlaceholders = (obj: any): any => { if (typeof obj === 'string') { // Replace ${1}, ${2}, etc. with slot values return obj.replace(/\$\{(\d+)\}/g, (_, num) => { return slots[`$${num}`] || '' }) } else if (Array.isArray(obj)) { return obj.map(item => replacePlaceholders(item)) } else if (typeof obj === 'object' && obj !== null) { const result: any = {} for (const [key, value] of Object.entries(obj)) { const newKey = replacePlaceholders(key) result[newKey] = replacePlaceholders(value) } return result } return obj } return replacePlaceholders(filled) } /** * Update pattern success metrics based on usage */ updateSuccessMetric(patternId: string, success: boolean): void { const current = this.successMetrics.get(patternId) || 0.5 // Exponential moving average const alpha = 0.1 const newMetric = success ? current + alpha * (1 - current) : current - alpha * current this.successMetrics.set(patternId, newMetric) } /** * Learn new pattern from successful query */ async learnPattern(query: string, result: any): Promise { // Find similar existing patterns const queryEmbedding = await this.getEmbedding(query) const similar = await this.findBestPatterns(queryEmbedding, 1) if (similar[0]?.similarity < 0.7) { // This is a new pattern type - add it const newPattern: Pattern = { id: `learned_${Date.now()}`, category: 'learned', examples: [query], pattern: this.generateRegexFromQuery(query), template: result, confidence: 0.6 // Start with moderate confidence } this.patterns.set(newPattern.id, newPattern) this.patternEmbeddings.set(newPattern.id, queryEmbedding) this.successMetrics.set(newPattern.id, 0.6) } else { // Similar pattern exists - add as example const pattern = similar[0].pattern if (!pattern.examples.includes(query)) { pattern.examples.push(query) // Update pattern embedding with new example const embeddings = await Promise.all( pattern.examples.map(ex => this.getEmbedding(ex)) ) const newEmbedding = this.averageVectors(embeddings) this.patternEmbeddings.set(pattern.id, newEmbedding) } } } /** * Helper: Average multiple vectors */ private averageVectors(vectors: Vector[]): Vector { if (vectors.length === 0) return [] const dim = vectors[0].length const avg = new Array(dim).fill(0) for (const vec of vectors) { for (let i = 0; i < dim; i++) { avg[i] += vec[i] } } for (let i = 0; i < dim; i++) { avg[i] /= vectors.length } return avg } /** * Helper: Calculate cosine similarity */ private cosineSimilarity(a: Vector, b: Vector): number { let dotProduct = 0 let normA = 0 let normB = 0 for (let i = 0; i < a.length; i++) { dotProduct += a[i] * b[i] normA += a[i] * a[i] normB += b[i] * b[i] } normA = Math.sqrt(normA) normB = Math.sqrt(normB) if (normA === 0 || normB === 0) return 0 return dotProduct / (normA * normB) } /** * Helper: Simple tokenization */ private tokenize(text: string): string[] { return text.toLowerCase().split(/\s+/).filter(t => t.length > 0) } /** * Helper: Post-process extracted slots */ private postProcessSlots(slots: Record, _pattern: Pattern): void { // Convert string numbers to actual numbers for (const [key, value] of Object.entries(slots)) { if (typeof value === 'string') { // Check if it's a number const num = parseFloat(value) if (!isNaN(num) && value.match(/^\d+(\.\d+)?$/)) { slots[key] = num } // Parse dates if (value.match(/\d{4}/) || value.match(/(january|february|march|april|may|june|july|august|september|october|november|december)/i)) { // Simple year extraction const year = value.match(/\d{4}/) if (year) { slots[key] = parseInt(year[0]) } } // Clean up captured values slots[key] = value.trim() } } } /** * Helper: Generate regex pattern from query */ private generateRegexFromQuery(query: string): string { // Simple pattern generation - replace variable parts with capture groups let pattern = query.toLowerCase() // Replace numbers with \d+ capture pattern = pattern.replace(/\d+/g, '(\\d+)') // Replace quoted strings with .+ capture pattern = pattern.replace(/"[^"]+"/g, '(.+)') // Replace proper nouns (capitalized words) with capture pattern = pattern.replace(/\b[A-Z]\w+\b/g, '([A-Z][\\w]+)') return pattern } /** * Get pattern statistics for monitoring */ getStatistics(): { totalPatterns: number categories: Record averageConfidence: number topPatterns: Array<{ id: string; success: number }> } { const stats = { totalPatterns: this.patterns.size, categories: {} as Record, averageConfidence: 0, topPatterns: [] as Array<{ id: string; success: number }> } // Count by category for (const pattern of this.patterns.values()) { stats.categories[pattern.category] = (stats.categories[pattern.category] || 0) + 1 } // Calculate average confidence let totalConfidence = 0 for (const confidence of this.successMetrics.values()) { totalConfidence += confidence } stats.averageConfidence = totalConfidence / this.successMetrics.size // Get top patterns by success const sortedPatterns = Array.from(this.successMetrics.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 10) stats.topPatterns = sortedPatterns.map(([id, success]) => ({ id, success })) return stats } }