UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

448 lines 17.3 kB
/** * Smart Field Mapper * * An intelligent field mapping system that learns from patterns and * automatically generates mappings for new fields. This prevents the * need for constant manual updates when new field patterns emerge. */ import { FIELDS } from '../generated/fields.generated.js'; import { FieldMapper } from './FieldMapper.js'; export class SmartFieldMapper extends FieldMapper { learnedPatterns = new Map(); fieldRelationships = []; unmappedFieldLog = new Map(); constructor() { super(); this.initializeSmartPatterns(); this.analyzeEntityRelationships(); } /** * Initialize smart patterns based on field analysis */ initializeSmartPatterns() { // Analyze all fields to find patterns const fieldPatterns = this.analyzeFieldPatterns(); // Common suffixes and their meanings const suffixMappings = [ { suffix: '_id', type: 'reference', confidence: 0.95 }, { suffix: '_key', type: 'reference', confidence: 0.95 }, { suffix: '_name', type: 'display', confidence: 0.9 }, { suffix: '_url', type: 'link', confidence: 0.9 }, { suffix: '_time', type: 'timestamp', confidence: 0.9 }, { suffix: '_date', type: 'timestamp', confidence: 0.9 }, { suffix: '_at', type: 'timestamp', confidence: 0.88 }, { suffix: '_enabled', type: 'boolean', confidence: 0.9 }, { suffix: '_count', type: 'numeric', confidence: 0.9 }, { suffix: '_percentage', type: 'numeric', confidence: 0.9 }, { suffix: '_threshold', type: 'numeric', confidence: 0.9 }, { suffix: '_list', type: 'array', confidence: 0.85 }, { suffix: '_ids', type: 'array', confidence: 0.9 }, { suffix: '_priorities', type: 'array', confidence: 0.9 } ]; // Common prefixes and their meanings const prefixMappings = [ { prefix: 'is_', type: 'boolean', confidence: 0.95 }, { prefix: 'has_', type: 'boolean', confidence: 0.95 }, { prefix: 'can_', type: 'boolean', confidence: 0.95 }, { prefix: 'should_', type: 'boolean', confidence: 0.9 }, { prefix: 'parent_', type: 'reference', confidence: 0.95 }, { prefix: 'child_', type: 'reference', confidence: 0.9 }, { prefix: 'related_', type: 'reference', confidence: 0.9 }, { prefix: 'default_', type: 'default_value', confidence: 0.9 }, { prefix: 'min_', type: 'constraint', confidence: 0.9 }, { prefix: 'max_', type: 'constraint', confidence: 0.9 }, { prefix: 'total_', type: 'aggregate', confidence: 0.9 }, { prefix: 'num_', type: 'count', confidence: 0.9 } ]; // Entity name patterns (for references) const entityNames = [ 'project', 'experiment', 'flag', 'feature', 'variation', 'event', 'audience', 'campaign', 'page', 'rule', 'ruleset', 'environment', 'webhook', 'group', 'layer', 'metric', 'attribute', 'extension', 'collaborator' ]; // Generate smart patterns for entity references for (const entity of entityNames) { // Direct entity_id pattern this.learnedPatterns.set(`${entity}_id_pattern`, { pattern: new RegExp(`^(${entity}[_\\s-]?(?:id|identifier))$`, 'i'), targetField: `${entity}_id`, confidence: 0.95, successCount: 0, source: 'inferred' }); // Parent entity pattern this.learnedPatterns.set(`parent_${entity}_pattern`, { pattern: new RegExp(`^(?:parent|associated|linked)[_\\s-]?${entity}(?:[_\\s-]?(?:id|key))?$`, 'i'), targetField: `${entity}_id`, confidence: 0.92, successCount: 0, source: 'inferred' }); } } /** * Analyze entity relationships from schema */ analyzeEntityRelationships() { // Map common relationship patterns this.fieldRelationships = [ { fromEntity: 'variation', toEntity: 'flag', fieldPattern: 'flag_key', examples: ['flag_key', 'parent_flag', 'feature_flag_key'] }, { fromEntity: 'variation', toEntity: 'experiment', fieldPattern: 'experiment_id', examples: ['experiment_id', 'parent_experiment', 'exp_id'] }, { fromEntity: 'experiment', toEntity: 'campaign', fieldPattern: 'campaign_id', examples: ['campaign_id', 'parent_campaign'] }, { fromEntity: 'experiment', toEntity: 'feature', fieldPattern: 'feature_id', examples: ['feature_id', 'feature_key', 'attached_feature'] }, { fromEntity: 'event', toEntity: 'page', fieldPattern: 'page_id', examples: ['page_id', 'parent_page', 'associated_page'] }, { fromEntity: 'rule', toEntity: 'layer', fieldPattern: 'layer_id', examples: ['layer_id', 'layer_experiment_id'] } ]; } /** * Analyze field patterns from schema */ analyzeFieldPatterns() { const patterns = new Map(); for (const entityName of Object.keys(FIELDS)) { const entity = FIELDS[entityName]; const fields = new Set(); if (entity.required) entity.required.forEach(f => fields.add(f)); if (entity.optional) entity.optional.forEach(f => fields.add(f)); patterns.set(entityName, fields); } return patterns; } /** * Override mapFields to add smart detection */ async mapFields(payload, options) { // First try standard mapping const standardMappings = await super.mapFields(payload, options); // For unmapped fields, try smart detection const payloadFields = Object.keys(payload); const mappedFields = new Set(standardMappings.map(m => m.aiFieldName)); const unmappedFields = payloadFields.filter(f => !mappedFields.has(f)); const smartMappings = []; for (const field of unmappedFields) { const smartMapping = this.detectSmartMapping(field, options.entityType); if (smartMapping) { smartMappings.push(smartMapping); // Log for learning this.logSuccessfulMapping(field, smartMapping.templateFieldName); } else { // Track unmapped fields this.trackUnmappedField(field); } } return [...standardMappings, ...smartMappings]; } /** * Detect smart mapping for a field */ detectSmartMapping(field, entityType) { // 1. Check learned patterns for (const [key, pattern] of this.learnedPatterns) { if (pattern.pattern.test(field)) { return { aiFieldName: field, templateFieldName: pattern.targetField, confidence: pattern.confidence, transformationType: 'custom' }; } } // 2. Check for entity references const entityRefMatch = this.detectEntityReference(field); if (entityRefMatch) { return entityRefMatch; } // 3. Check for suffix patterns const suffixMatch = this.detectSuffixPattern(field); if (suffixMatch) { return suffixMatch; } // 4. Check for prefix patterns const prefixMatch = this.detectPrefixPattern(field); if (prefixMatch) { return prefixMatch; } // 5. Use edit distance for close matches const closeMatch = this.findClosestMatch(field, entityType); if (closeMatch && closeMatch.confidence > 0.7) { return closeMatch; } return null; } /** * Detect entity reference patterns */ detectEntityReference(field) { const fieldLower = field.toLowerCase(); for (const relationship of this.fieldRelationships) { // Check if field contains the entity name if (fieldLower.includes(relationship.toEntity)) { // Check for ID/key suffix if (fieldLower.includes('_id') || fieldLower.includes('id')) { return { aiFieldName: field, templateFieldName: `${relationship.toEntity}_id`, confidence: 0.9, transformationType: 'custom' }; } if (fieldLower.includes('_key') || fieldLower.includes('key')) { return { aiFieldName: field, templateFieldName: `${relationship.toEntity}_key`, confidence: 0.9, transformationType: 'custom' }; } } } return null; } /** * Detect suffix patterns */ detectSuffixPattern(field) { const suffixPatterns = [ { suffix: '_id', confidence: 0.85 }, { suffix: '_key', confidence: 0.85 }, { suffix: '_time', confidence: 0.8 }, { suffix: '_date', confidence: 0.8 }, { suffix: '_url', confidence: 0.8 }, { suffix: '_enabled', confidence: 0.85 }, { suffix: '_count', confidence: 0.8 } ]; for (const pattern of suffixPatterns) { if (field.endsWith(pattern.suffix)) { // Try to find the base field const baseField = field.substring(0, field.length - pattern.suffix.length); // Check if this could be a known field const targetField = this.inferTargetField(baseField, pattern.suffix); if (targetField) { return { aiFieldName: field, templateFieldName: targetField, confidence: pattern.confidence, transformationType: 'custom' }; } } } return null; } /** * Detect prefix patterns */ detectPrefixPattern(field) { const prefixPatterns = [ { prefix: 'is_', type: 'boolean', confidence: 0.9 }, { prefix: 'has_', type: 'boolean', confidence: 0.9 }, { prefix: 'parent_', type: 'reference', confidence: 0.85 }, { prefix: 'default_', type: 'default', confidence: 0.85 } ]; for (const pattern of prefixPatterns) { if (field.startsWith(pattern.prefix)) { const baseField = field.substring(pattern.prefix.length); // For boolean fields, often the target is the same if (pattern.type === 'boolean') { return { aiFieldName: field, templateFieldName: field, confidence: pattern.confidence, transformationType: 'direct' }; } // For references, try to find the entity if (pattern.type === 'reference') { const targetField = this.inferReferenceField(baseField); if (targetField) { return { aiFieldName: field, templateFieldName: targetField, confidence: pattern.confidence, transformationType: 'custom' }; } } } } return null; } /** * Find closest match using edit distance */ findClosestMatch(field, entityType) { const entity = FIELDS[entityType]; if (!entity) return null; const allFields = new Set(); if (entity.required) entity.required.forEach(f => allFields.add(f)); if (entity.optional) entity.optional.forEach(f => allFields.add(f)); let bestMatch = null; for (const targetField of allFields) { const distance = this.editDistance(field.toLowerCase(), targetField.toLowerCase()); const similarity = 1 - (distance / Math.max(field.length, targetField.length)); if (similarity > 0.7) { if (!bestMatch || distance < bestMatch.distance) { bestMatch = { field: targetField, distance }; } } } if (bestMatch) { const similarity = 1 - (bestMatch.distance / Math.max(field.length, bestMatch.field.length)); return { aiFieldName: field, templateFieldName: bestMatch.field, confidence: similarity, transformationType: 'custom' }; } return null; } /** * Calculate edit distance between two strings */ editDistance(a, b) { const matrix = []; for (let i = 0; i <= b.length; i++) { matrix[i] = [i]; } for (let j = 0; j <= a.length; j++) { matrix[0][j] = j; } for (let i = 1; i <= b.length; i++) { for (let j = 1; j <= a.length; j++) { if (b.charAt(i - 1) === a.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1); } } } return matrix[b.length][a.length]; } /** * Infer target field based on patterns */ inferTargetField(baseField, suffix) { // Common inferences if (suffix === '_id') { // Check if base is an entity name const entities = ['project', 'experiment', 'flag', 'feature', 'campaign', 'page']; if (entities.includes(baseField)) { return `${baseField}_id`; } } // Direct match for some common patterns const directMatches = { 'created_time': 'created', 'updated_time': 'last_modified', 'modified_time': 'last_modified', 'start_date': 'start_time', 'end_date': 'end_time' }; return directMatches[baseField + suffix] || null; } /** * Infer reference field from base */ inferReferenceField(baseField) { // If it ends with an entity name, add _id const entities = ['project', 'experiment', 'flag', 'feature', 'campaign']; for (const entity of entities) { if (baseField === entity || baseField.endsWith(`_${entity}`)) { return `${entity}_id`; } } return null; } /** * Log successful mapping for learning */ logSuccessfulMapping(fromField, toField) { this.logger.debug({ action: 'smart_mapping_success', from: fromField, to: toField }, 'SmartFieldMapper: Successful smart mapping'); } /** * Track unmapped fields for analysis */ trackUnmappedField(field) { const count = this.unmappedFieldLog.get(field) || 0; this.unmappedFieldLog.set(field, count + 1); // Alert if field appears frequently if (count + 1 === 5) { this.logger.warn({ field, occurrences: count + 1 }, 'SmartFieldMapper: Frequently unmapped field detected'); } } /** * Get unmapped field report */ getUnmappedFieldReport() { return Array.from(this.unmappedFieldLog.entries()) .map(([field, count]) => ({ field, count })) .sort((a, b) => b.count - a.count); } /** * Generate mapping recommendations */ generateRecommendations() { const recommendations = []; for (const [field, count] of this.unmappedFieldLog) { if (count >= 3) { // Try to infer a mapping const smartMapping = this.detectSmartMapping(field, 'generic'); if (smartMapping) { recommendations.push({ field, suggestedMapping: smartMapping.templateFieldName, confidence: smartMapping.confidence }); } } } return recommendations; } } //# sourceMappingURL=SmartFieldMapper.js.map