UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

926 lines (925 loc) 64.2 kB
/** * Intelligent Payload Parser * @description Transforms AI-generated payloads into correct API formats before validation * * Purpose: Eliminate template mode friction by intelligently parsing creative AI payloads * and automatically converting them to the expected format for orchestration. * * Key Features: * - Non-destructive: Graceful fallback to template mode if parsing fails * - Pattern recognition: Detects page, event, variation patterns in payloads * - Field mapping: Maps AI field names to template field names using fuzzy matching * - Structure analysis: Finds data in deeply nested payload structures * - Format transformation: Converts complex objects to API-expected formats * * @author Optimizely MCP Server * @version 1.0.0 */ import { JSONPath } from 'jsonpath-plus'; import { getLogger } from '../logging/Logger.js'; import { PatternRecognitionEngine } from './PatternRecognitionEngine.js'; import { FieldMapper } from './FieldMapper.js'; import { TemplateAutoFiller } from './TemplateAutoFiller.js'; import { RulesetPayloadTransformer } from './RulesetPayloadTransformer.js'; import { rulesetUpdateTransformer } from './RulesetUpdateTransformer.js'; import { JSONataFieldExtractor } from './JSONataFieldExtractor.js'; /** * Main Intelligent Payload Parser class */ export class IntelligentPayloadParser { logger = getLogger(); fuse = null; patternEngine; fieldMapper; templateAutoFiller; rulesetTransformer; jsonataExtractor; constructor() { this.patternEngine = new PatternRecognitionEngine(); this.fieldMapper = new FieldMapper(); this.templateAutoFiller = new TemplateAutoFiller(); this.rulesetTransformer = new RulesetPayloadTransformer(); this.jsonataExtractor = new JSONataFieldExtractor(); this.logger.debug('IntelligentPayloadParser initialized'); } /** * Main entry point: Parse and transform an AI-generated payload */ async parsePayload(payload, options) { try { // TEMPLATE MARKER DETECTION: Check for template marker field first if (payload._template_type) { this.logger.info({ templateType: payload._template_type, originalPayload: payload, operation: options.operation, entityType: options.entityType }, 'IntelligentPayloadParser: Template marker detected - preserving original field structure'); // Return payload with minimal transformation (just remove the marker) const cleanPayload = { ...payload }; delete cleanPayload._template_type; return { success: true, transformedPayload: cleanPayload, confidence: 1.0, appliedTransformations: ['template_marker_removal'], suggestions: [`Template type '${payload._template_type}' detected - field structure preserved`] }; } // Log the incoming payload for variation processing if (options.entityType === 'variation') { this.logger.debug({ operation: options.operation, entityType: options.entityType, originalPayload: payload, hasKey: !!payload?.key, hasName: !!payload?.name, keyValue: payload?.key, nameValue: payload?.name }, 'IntelligentPayloadParser: Processing variation payload'); } // CRITICAL FIX: Handle variation creation with confused key/flag_key fields if (options.operation === 'create' && options.entityType === 'variation' && payload) { const transformed = await this.fixVariationKeyConfusion(payload); if (transformed !== payload) { this.logger.debug({ originalPayload: payload, transformedPayload: transformed, keyChanged: payload.key !== transformed.key, nameChanged: payload.name !== transformed.name }, 'IntelligentPayloadParser: Applied variation key confusion fix'); return { success: true, transformedPayload: transformed, confidence: 0.85, appliedTransformations: ['variation_key_confusion_fix'], suggestions: ['Detected and fixed variation key confusion - moved flag key to flag_key field'] }; } } // Handle JSON Patch arrays for update operations if (Array.isArray(payload) && options.operation === 'update' && options.entityType === 'ruleset') { this.logger.debug('Detected JSON Patch array for ruleset update - checking for Web vs Feature format'); // First check if this is a Web-style ruleset update that needs transformation const transformResult = rulesetUpdateTransformer.transformRulesetUpdate(payload, options.platform || 'feature', {} // EntityRouter will provide proper context with flag_key and environment_key ); if (!transformResult.success) { return { success: false, errors: transformResult.errors, suggestions: transformResult.guidance ? [transformResult.guidance] : [], confidence: 0.95 }; } // Apply any transformations const transformedPayload = transformResult.transformed || payload; // Then apply path corrections const correctedPatch = await this.correctJsonPatchPaths(transformedPayload, options); return { success: true, transformedPayload: correctedPatch, confidence: 0.9, appliedTransformations: ['web_to_feature_transformation', 'json_patch_path_correction'], suggestions: ['Transformed Web-style ruleset update to Feature Experimentation format'] }; } // INTELLIGENT RULESET UPDATE DETECTION - Handle ANY creative format if (options.operation === 'update' && options.entityType === 'ruleset' && !Array.isArray(payload)) { this.logger.info({ payloadType: typeof payload, keys: Object.keys(payload || {}), sampleData: JSON.stringify(payload).substring(0, 200) }, 'Detecting ruleset update format in non-array payload'); // Strategy 1: Deep search for JSON Patch arrays using JSONPath const jsonPatchPaths = JSONPath({ path: '$..*[?(@.op && @.path)]', json: payload, resultType: 'parent' }); if (jsonPatchPaths.length > 0) { // Found JSON Patch operations somewhere in the structure let patchArray = []; // Check if the parent is already an array of patches const firstResult = jsonPatchPaths[0]; if (Array.isArray(firstResult)) { patchArray = firstResult; } else { // Individual patch objects found, collect them patchArray = jsonPatchPaths.filter((item) => item.op && item.path && ['add', 'remove', 'replace', 'move', 'copy', 'test'].includes(item.op)); } this.logger.info({ foundPatches: patchArray.length, searchDepth: 'deep', firstPatch: patchArray[0] }, 'Found JSON Patch operations via deep search'); const correctedPatch = await this.correctJsonPatchPaths(patchArray, options); return { success: true, transformedPayload: correctedPatch, confidence: 0.8, appliedTransformations: ['deep_json_patch_extraction', 'json_patch_path_correction'], suggestions: ['Send JSON Patch operations as a direct array at the root level for better performance'] }; } // Strategy 2: Look for ruleset-like structures (rules, variations, etc.) const rulesetPatterns = [ '$.ruleset_data', '$.ruleset', '$.data', '$.patch', '$.patches', '$.operations', '$.update', '$.updates', '$.changes', '$.modifications', '$.rules_update' ]; for (const pattern of rulesetPatterns) { const results = JSONPath({ path: pattern, json: payload }); if (results.length > 0 && results[0]) { const potentialData = results[0]; // Check if it's a JSON Patch array if (Array.isArray(potentialData) && potentialData.length > 0) { const isJsonPatch = potentialData.every((item) => item.op && item.path && typeof item.op === 'string'); if (isJsonPatch) { this.logger.info({ pattern, opsCount: potentialData.length }, `Found JSON Patch array at ${pattern}`); const correctedPatch = await this.correctJsonPatchPaths(potentialData, options); return { success: true, transformedPayload: correctedPatch, confidence: 0.85, appliedTransformations: ['pattern_based_extraction', 'json_patch_path_correction'], suggestions: [`Remove the wrapper '${pattern.substring(2)}' and send JSON Patch array directly`] }; } } // Check if it's a ruleset object that needs conversion if (typeof potentialData === 'object' && !Array.isArray(potentialData)) { const converted = await this.convertRulesetObjectToJsonPatch(potentialData, options); if (converted.length > 0) { this.logger.info({ pattern, convertedOps: converted.length }, `Converted ruleset object at ${pattern} to JSON Patch`); return { success: true, transformedPayload: converted, confidence: 0.75, appliedTransformations: ['ruleset_object_to_json_patch', 'json_patch_path_correction'], suggestions: ['Use JSON Patch format directly for ruleset updates'] }; } } } } // Strategy 3: Check if the payload itself is a ruleset object if (payload.rules || payload.enabled !== undefined || payload.default_variation_key) { const converted = await this.convertRulesetObjectToJsonPatch(payload, options); if (converted.length > 0) { this.logger.info({ convertedOps: converted.length }, 'Converted root ruleset object to JSON Patch'); return { success: true, transformedPayload: converted, confidence: 0.7, appliedTransformations: ['direct_ruleset_to_json_patch'], suggestions: ['Use JSON Patch format for ruleset updates: [{"op": "replace", "path": "/rules/...", "value": {...}}]'] }; } } // Strategy 4: Look for variations updates in various formats // CRITICAL FIX: Skip this strategy for experiments since they have variations at the root level // This prevents experiments from being misidentified as ruleset updates if (options.entityType.toLowerCase() === 'experiment') { this.logger.debug('Skipping variations detection for experiment entity type'); // Continue to next strategy } else { const variationPatterns = [ '$.variations', '$.*.variations', '$..variations', '$.rules.*.variations', '$..rules.*.variations' ]; for (const pattern of variationPatterns) { const results = JSONPath({ path: pattern, json: payload, resultType: 'all' }); if (results.length > 0) { // Found variations somewhere, create patches for them const patches = []; for (const result of results) { const parentPath = JSONPath.toPathString(result.path.slice(0, -1)); const variations = result.value; // Extract rule key from path if possible const ruleKeyMatch = parentPath.match(/rules\[['"]([^'"]+)['"]\]/); const ruleKey = ruleKeyMatch ? ruleKeyMatch[1] : 'unknown_rule'; if (variations) { patches.push({ op: 'replace', path: `/rules/${ruleKey}/variations`, value: Array.isArray(variations) ? variations : Object.values(variations) }); } } if (patches.length > 0) { const correctedPatch = await this.correctJsonPatchPaths(patches, options); return { success: true, transformedPayload: correctedPatch, confidence: 0.65, appliedTransformations: ['variations_extraction_to_json_patch'], suggestions: ['Detected variation updates - converted to JSON Patch format'] }; } } } } // Close the else block for Strategy 4 // No valid update format found return { success: false, errors: ['Could not detect valid ruleset update format in payload'], suggestions: [ 'Use JSON Patch format: [{"op": "replace", "path": "/rules/rule_key", "value": {...}}]', 'Or wrap in standard fields: { "patches": [...] } or { "ruleset_data": [...] }', 'See documentation for valid ruleset update formats' ], confidence: 0 }; } this.logger.debug({ entityType: options.entityType, operation: options.operation, keys: Object.keys(payload || {}), hasAbTest: !!payload?.ab_test, abTestVariationCount: payload?.ab_test?.variations?.length || 0, abTestVariationKeys: payload?.ab_test?.variations?.map((v) => v.key) || [] }, `Starting payload parsing for ${options.entityType}:${options.operation}`); // Step 0: Use JSONata to extract and normalize fields first const extractionResult = await this.jsonataExtractor.extractFields(options.entityType, payload, { platform: options.platform }); if (extractionResult.success && extractionResult.confidence > 0.7) { this.logger.debug({ entityType: options.entityType, transformations: extractionResult.transformations.length, confidence: extractionResult.confidence, beforeExtraction: { hasAbTest: !!payload?.ab_test, abTestVariationCount: payload?.ab_test?.variations?.length || 0 }, afterExtraction: { hasAbTest: !!extractionResult.extractedFields?.ab_test, abTestVariationCount: extractionResult.extractedFields?.ab_test?.variations?.length || 0 } }, 'JSONataFieldExtractor successfully normalized fields'); // CRITICAL DEBUG: Track description field transformation if (options.entityType === 'flag') { this.logger.info({ originalDescription: payload.description, originalDescriptionType: typeof payload.description, extractedDescription: extractionResult.extractedFields.description, extractedDescriptionType: typeof extractionResult.extractedFields.description, extractedDescriptionIsArray: Array.isArray(extractionResult.extractedFields.description), extractedKeys: Object.keys(extractionResult.extractedFields) }, 'DESCRIPTION BUG TRACE: JSONataFieldExtractor result'); } // CRITICAL FIX: Merge extracted fields with original payload to preserve complex fields // like ab_test that aren't in the base schema but are valid for template operations payload = { ...payload, // Preserve original fields (ab_test, etc.) ...extractionResult.extractedFields // Overlay normalized fields }; } // Step 1: Detect patterns in the payload const analysis = await this.patternEngine.analyzePayload(payload); const patterns = analysis.structurePatterns; // Step 2: Map fields using fuzzy matching const fieldMappings = await this.fieldMapper.mapFields(payload, { entityType: options.entityType, threshold: 0.6, enableTransformations: true, strictMode: options.strict || false }); // CRITICAL DEBUG: Track field mappings if (options.entityType === 'flag') { const descriptionMapping = fieldMappings.find(m => m.templateFieldName === 'description'); if (descriptionMapping) { this.logger.info({ descriptionMapping: { aiFieldName: descriptionMapping.aiFieldName, templateFieldName: descriptionMapping.templateFieldName, confidence: descriptionMapping.confidence } }, 'DESCRIPTION BUG TRACE: Found description field mapping'); } } // Step 3: Transform the payload structure // CRITICAL DEBUG: Track payload before transformation if (options.entityType === 'flag') { this.logger.info({ beforeTransform: { description: payload.description, descriptionType: typeof payload.description, hasAbTest: !!payload.ab_test, abTestVariations: payload.ab_test?.variations?.length || 0 } }, 'DESCRIPTION BUG TRACE: Before transformPayload'); } const transformedPayload = await this.transformPayload(payload, fieldMappings, patterns, options); // CRITICAL DEBUG: Track payload after transformation if (options.entityType === 'flag') { this.logger.info({ afterTransform: { description: transformedPayload.description, descriptionType: typeof transformedPayload.description, descriptionIsArray: Array.isArray(transformedPayload.description), hasAbTest: !!transformedPayload.ab_test, abTestVariations: transformedPayload.ab_test?.variations?.length || 0 } }, 'DESCRIPTION BUG TRACE: After transformPayload'); } // Step 4: Auto-fill template with transformed data // CRITICAL FIX: Skip auto-fill for UPDATE operations to preserve all fields // The auto-filler only knows about CREATE fields and strips everything else let finalPayload = transformedPayload; let confidence = 0.85; // High confidence for UPDATE operations let allErrors = []; let allSuggestions = []; if (options.operation !== 'update') { const autoFillResult = await this.templateAutoFiller.autoFillTemplate(transformedPayload, fieldMappings, analysis, { entityType: options.entityType, operation: options.operation, platform: options.platform, minimumConfidence: 0.6, validateAfterFill: true, allowPartialFill: !options.strict }); finalPayload = autoFillResult.success ? autoFillResult.filledTemplate : transformedPayload; allErrors = [...(autoFillResult.validationErrors || [])]; allSuggestions = [...(autoFillResult.suggestions || [])]; confidence = autoFillResult.confidence; } else { // For UPDATE operations, preserve all fields this.logger.debug({ operation: options.operation, entityType: options.entityType, preservedFields: Object.keys(finalPayload), hasAbTest: !!finalPayload.ab_test, abTestVariationCount: finalPayload.ab_test?.variations?.length || 0, abTestVariationKeys: finalPayload.ab_test?.variations?.map((v) => v.key) || [] }, 'Skipping auto-fill for UPDATE operation to preserve all fields'); } // Step 5: Final validation and result compilation const result = { success: options.operation === 'update' || confidence >= 0.6, transformedPayload: finalPayload, errors: allErrors.length > 0 ? allErrors : undefined, suggestions: allSuggestions.length > 0 ? allSuggestions : undefined, confidence: confidence }; this.logger.debug(`Payload parsing completed: success=${result.success}, confidence=${result.confidence}`); return result; } catch (error) { this.logger.error(`Payload parsing failed for ${options.entityType}:${options.operation}: ${error instanceof Error ? error.message : 'Unknown error'}`); return { success: false, errors: [`Parsing failed: ${error instanceof Error ? error.message : 'Unknown error'}`], confidence: 0 }; } } /** * Transform the payload structure based on mappings and patterns */ async transformPayload(payload, fieldMappings, patterns, options) { let transformed = { ...payload }; // Apply field mappings first if (fieldMappings.length > 0) { this.logger.debug({ entityType: options.entityType, operation: options.operation, beforeFieldMapping: { hasAbTest: !!transformed.ab_test, abTestVariationCount: transformed.ab_test?.variations?.length || 0, abTestVariationKeys: transformed.ab_test?.variations?.map((v) => v.key) || [] }, fieldMappingsCount: fieldMappings.length, fieldMappings: fieldMappings.map(fm => ({ aiField: fm.aiFieldName, templateField: fm.templateFieldName, confidence: fm.confidence })) }, 'About to apply field mappings'); transformed = await this.fieldMapper.applyMappings(transformed, fieldMappings); this.logger.debug({ entityType: options.entityType, operation: options.operation, afterFieldMapping: { hasAbTest: !!transformed.ab_test, abTestVariationCount: transformed.ab_test?.variations?.length || 0, abTestVariationKeys: transformed.ab_test?.variations?.map((v) => v.key) || [] } }, 'Applied field mappings'); } // Apply structure-specific transformations using JSONPath for (const pattern of patterns) { transformed = await this.applyStructureTransformation(transformed, pattern, options); } // Handle special cases based on entity type and operation if (options.entityType === 'ruleset' && options.operation === 'update_ruleset') { // Use specialized ruleset transformer for better results const rulesetResult = await this.rulesetTransformer.transformRulesetPayload(transformed); if (rulesetResult.success && rulesetResult.transformedRuleset) { transformed = rulesetResult.transformedRuleset; } else { // Fallback to basic transformation transformed = await this.transformRulesetStructure(transformed); } } // Handle event type routing for non-custom events if (options.entityType === 'event' && options.operation === 'create') { transformed = await this.handleEventTypeRouting(transformed); } // CRITICAL FIX: Handle flag update with ab_test structure // When updating a flag with variations/metrics at root level, wrap in ab_test if (options.entityType === 'flag' && options.operation === 'update') { transformed = await this.handleFlagAbTestStructure(transformed); } this.logger.debug(`Payload transformation completed with ${Object.keys(transformed).length} fields`); return transformed; } /** * Apply structure-specific transformations using JSONPath */ async applyStructureTransformation(payload, pattern, options) { let transformed = { ...payload }; switch (pattern.pattern) { case 'nested_ruleset_data': transformed = await this.flattenNestedRulesetData(transformed); break; case 'array_rules_structure': transformed = await this.wrapRulesInRulesetStructure(transformed); break; case 'ruleset_transformation': transformed = await this.normalizeRulesetFields(transformed); break; default: this.logger.debug(`No transformation handler for pattern: ${pattern.pattern}`); } return transformed; } /** * Flatten nested ruleset_data to root level using JSONPath */ async flattenNestedRulesetData(payload) { const transformed = { ...payload }; // Use JSONPath to find nested ruleset data const rulesetDataPaths = JSONPath({ path: '$..ruleset_data', json: payload, resultType: 'path' }); for (const path of rulesetDataPaths) { const pathString = JSONPath.toPathString(path); const rulesetData = JSONPath({ path: pathString, json: payload })[0]; if (rulesetData && typeof rulesetData === 'object') { // Extract common ruleset fields to root level if (rulesetData.enabled !== undefined) transformed.enabled = rulesetData.enabled; if (rulesetData.rules) transformed.rules = rulesetData.rules; if (rulesetData.default_variation_key) transformed.default_variation_key = rulesetData.default_variation_key; if (rulesetData.rollout_id) transformed.rollout_id = rulesetData.rollout_id; if (rulesetData.archived !== undefined) transformed.archived = rulesetData.archived; // Remove the nested structure delete transformed.ruleset_data; } } return transformed; } /** * Wrap rules array in proper ruleset structure */ async wrapRulesInRulesetStructure(payload) { const transformed = { ...payload }; if (Array.isArray(transformed.rules)) { // Ensure we have required ruleset fields if (transformed.enabled === undefined) transformed.enabled = true; if (!transformed.default_variation_key) { // Try to infer from first rule's variations const firstRule = transformed.rules[0]; if (firstRule && firstRule.variations && firstRule.variations.length > 0) { transformed.default_variation_key = firstRule.variations[0].key; } } } return transformed; } /** * Normalize ruleset field names and structure */ async normalizeRulesetFields(payload) { const transformed = { ...payload }; // Common field normalizations using JSONPath queries const enabledPaths = JSONPath({ path: '$..enabled', json: payload, resultType: 'path' }); const isActivePaths = JSONPath({ path: '$..is_active', json: payload, resultType: 'path' }); // Prefer 'enabled' over 'is_active' if (isActivePaths.length > 0 && enabledPaths.length === 0) { const isActiveValue = JSONPath({ path: JSONPath.toPathString(isActivePaths[0]), json: payload })[0]; transformed.enabled = isActiveValue; // Remove is_active field const parentPath = isActivePaths[0].slice(0, -1); const parent = JSONPath({ path: JSONPath.toPathString(parentPath), json: transformed })[0]; if (parent && typeof parent === 'object') { delete parent.is_active; } } return transformed; } /** * Transform ruleset structure for API compatibility */ async transformRulesetStructure(payload) { const transformed = { ...payload }; // Ensure we have a complete ruleset structure if (!transformed.enabled && transformed.enabled !== false) { transformed.enabled = true; // Default to enabled } // Ensure rules array exists if (!transformed.rules) { transformed.rules = []; } // Validate default_variation_key exists if (!transformed.default_variation_key && transformed.rules.length > 0) { const firstRuleWithVariations = transformed.rules.find((rule) => rule.variations && rule.variations.length > 0); if (firstRuleWithVariations) { transformed.default_variation_key = firstRuleWithVariations.variations[0].key; } } return transformed; } /** * Handle event type routing for non-custom events * @description Detects when event_type is not "custom" and transforms payload for page event endpoint */ async handleEventTypeRouting(payload) { const transformed = { ...payload }; // If event_type is not specified or is 'custom', no transformation needed if (!transformed.event_type || transformed.event_type === 'custom') { return transformed; } // For non-custom events (click, pageview, etc.), we need to route to page events endpoint // This requires a page_id to be present if (!transformed.page_id) { this.logger.warn(`Event with type '${transformed.event_type}' requires a page_id but none was provided`); // Add a special marker to indicate this needs page event routing transformed._requiresPageEventEndpoint = true; transformed._missingPageId = true; // Try to extract page_id from other fields if (transformed.page || transformed.page_key) { transformed.page_id = transformed.page || transformed.page_key; delete transformed._missingPageId; this.logger.info(`Auto-extracted page_id from ${transformed.page ? 'page' : 'page_key'} field`); } } else { // Page ID is present, just mark for routing transformed._requiresPageEventEndpoint = true; } // Ensure event_type is valid for page events const validPageEventTypes = ['click', 'pageview', 'custom', 'touch', 'submission']; if (!validPageEventTypes.includes(transformed.event_type)) { this.logger.warn(`Event type '${transformed.event_type}' may not be valid for page events. Valid types: ${validPageEventTypes.join(', ')}`); // Auto-correct common mistakes const eventTypeMapping = { 'clicked': 'click', 'tap': 'touch', 'tapped': 'touch', 'submit': 'submission', 'submitted': 'submission', 'form': 'submission', 'view': 'pageview', 'viewed': 'pageview', 'page_view': 'pageview' }; const correctedType = eventTypeMapping[transformed.event_type.toLowerCase()]; if (correctedType) { this.logger.info(`Auto-correcting event_type from '${transformed.event_type}' to '${correctedType}'`); transformed.event_type = correctedType; transformed._eventTypeCorrected = true; } } return transformed; } /** * Handle flag update with ab_test structure * @description Detects when variations/metrics are at root level and wraps them in ab_test */ async handleFlagAbTestStructure(payload) { let transformed = { ...payload }; // If already has ab_test structure, just return if (transformed.ab_test) { this.logger.debug('Payload already has ab_test structure, no transformation needed'); return transformed; } // Check if payload has variations/metrics at root level (common AI agent mistake) const hasRootVariations = transformed.variations && Array.isArray(transformed.variations); const hasRootMetrics = transformed.metrics && Array.isArray(transformed.metrics); const hasRootEnvironment = transformed.environment || transformed.environment_key; const hasRootAudience = transformed.audience || transformed.audience_conditions; // If we have any ab_test-like fields at root, wrap them if (hasRootVariations || hasRootMetrics || hasRootEnvironment || hasRootAudience) { this.logger.info({ hasRootVariations, hasRootMetrics, hasRootEnvironment, hasRootAudience, variationCount: transformed.variations?.length || 0, metricCount: transformed.metrics?.length || 0 }, 'Detected ab_test fields at root level, wrapping in ab_test structure'); // Build the ab_test structure const abTest = {}; // Move variations if (hasRootVariations) { abTest.variations = transformed.variations; delete transformed.variations; } // Move metrics if (hasRootMetrics) { abTest.metrics = transformed.metrics; delete transformed.metrics; } // Move environment if (transformed.environment) { abTest.environment = transformed.environment; delete transformed.environment; } else if (transformed.environment_key) { abTest.environment = transformed.environment_key; delete transformed.environment_key; } // Move audience conditions if (transformed.audience_conditions) { abTest.audience_conditions = transformed.audience_conditions; delete transformed.audience_conditions; } else if (transformed.audience) { abTest.audience_conditions = transformed.audience; delete transformed.audience; } // Move rule-related fields if (transformed.rule_key) { abTest.rule_key = transformed.rule_key; delete transformed.rule_key; } if (transformed.rule_name) { abTest.rule_name = transformed.rule_name; delete transformed.rule_name; } // Add the ab_test to transformed payload transformed.ab_test = abTest; this.logger.info({ abTestKeys: Object.keys(abTest), remainingKeys: Object.keys(transformed), abTestStructure: abTest }, 'Successfully wrapped fields in ab_test structure'); } return transformed; } /** * Specialized handler for ruleset payload transformations */ async transformRulesetPayload(payload) { this.logger.debug(`Transforming ruleset payload with ${Object.keys(payload || {}).length} fields`); try { // Use specialized ruleset transformer first const rulesetResult = await this.rulesetTransformer.transformRulesetPayload(payload); if (rulesetResult.success && rulesetResult.transformedRuleset) { return { success: true, transformedPayload: rulesetResult.transformedRuleset, confidence: rulesetResult.confidence, suggestions: rulesetResult.warnings }; } else { // Fallback to general parser if specialized transformer fails this.logger.debug('Specialized ruleset transformer failed, falling back to general parser'); const result = await this.parsePayload(payload, { entityType: 'ruleset', operation: 'update_ruleset', platform: 'feature', enableFuzzyMatching: true }); return result; } } catch (error) { this.logger.error(`Ruleset transformation failed: ${error instanceof Error ? error.message : 'Unknown error'}`); return { success: false, errors: [`Ruleset transformation failed: ${error instanceof Error ? error.message : 'Unknown error'}`], confidence: 0 }; } } /** * Get field mapping suggestions for a given entity type */ async getFieldMappingSuggestions(entityType, aiFieldNames) { return await this.fieldMapper.mapFields(Object.fromEntries(aiFieldNames.map(name => [name, null])), { entityType, threshold: 0.8 }); } /** * Analyze payload structure and provide insights */ async analyzePayloadStructure(payload) { // TODO: Implement structure analysis // - Calculate payload complexity // - Identify potential entity types // - Suggest transformations return { depth: 0, fieldCount: 0, suspectedEntityTypes: [], recommendedTransformations: [] }; } /** * Correct JSON Patch paths for ruleset updates * Fixes common mistakes like /rules/{key}/variations -> /rules/{key}/actions/0/changes/0/value/variations */ async correctJsonPatchPaths(patchOps, options) { this.logger.info({ originalOps: patchOps, opsCount: patchOps.length }, 'Starting JSON Patch path correction'); const correctedOps = await Promise.all(patchOps.map(async (op) => { if (!op.path) return op; // Fix rule addition with wrong structure for Feature Experimentation const ruleAddMatch = op.path.match(/^\/rules\/([^\/]+)$/); if (ruleAddMatch && op.op === 'add' && op.value && typeof op.value === 'object') { const ruleKey = ruleAddMatch[1]; const ruleValue = op.value; // Check if this is a simplified rule structure that needs conversion if (ruleValue.variations && !ruleValue.actions) { this.logger.info({ originalPath: op.path, ruleKey, hasVariations: true, hasActions: false, variationsType: Array.isArray(ruleValue.variations) ? 'array' : 'object' }, 'Converting simplified rule structure to Feature Experimentation format'); // Convert variations from object to array if needed let variationsArray = ruleValue.variations; if (!Array.isArray(ruleValue.variations) && typeof ruleValue.variations === 'object') { this.logger.info({ variationKeys: Object.keys(ruleValue.variations) }, 'Converting variations from object to array format'); variationsArray = Object.entries(ruleValue.variations).map(([varKey, varData]) => { // If the variation data is just the percentage, convert it if (typeof varData === 'number') { return { key: varKey, percentage_included: varData }; } // Otherwise ensure it has the key field return { key: varKey, ...varData }; }); } // Convert simplified structure to proper FX structure const properRule = { key: ruleValue.key || ruleKey, name: ruleValue.name, type: ruleValue.type || 'a/b', percentage_included: ruleValue.percentage_included || 10000, audience_conditions: ruleValue.audience_conditions || [], enabled: ruleValue.enabled !== undefined ? ruleValue.enabled : true, actions: [{ changes: [{ type: 'apply_feature_variables', value: { variations: variationsArray } }] }] }; // Copy any metrics if present if (ruleValue.metrics) { properRule.metrics = ruleValue.metrics; } return { ...op, value: properRule }; } } // Fix variation paths that are missing the actions/changes structure const variationPathMatch = op.path.match(/^\/rules\/([^\/]+)\/variations$/); if (variationPathMatch) { const ruleKey = variationPathMatch[1]; this.logger.info({ originalPath: op.path, operation: op.op, value: op.value }, `Correcting variation path for rule ${ruleKey}`); // If trying to replace entire variations array, convert to individual updates if (op.op === 'replace' && Array.isArray(op.value)) { // Check if this is a Feature Experimentation variations format // FX variations use { key: string, percentage_included: number } format // But AI agents might use many variations of the field name const percentageFieldVariations = [ 'percentage', 'percent', 'pct', 'weight', 'allocation', 'traffic', 'traffic_percentage', 'traffic_allocation', 'traffic_weight', 'traffic_split', 'split_percentage', 'percentage_included', 'percent_included', 'inclusion_percentage', 'rollout_percentage', 'rollout_percent', 'exposure', 'exposure_percentage', 'distribution', 'distribution_percentage', 'serving_percentage', 'serving_percent', 'variant_weight', 'variation_weight', 'variation_percentage', 'bucket_percentage', 'sample_rate', 'ramp', 'rollout_pct', 'weight_total' ]; const isFXVariations = op.value.length > 0 && op.value.every((v) => { if (!v.key) return false; // Check if the variation has any percentage-related field return Object.keys(v).some(key => key === 'percentage_included' || percentageFieldVariations.includes(key.toLowerCase().replace(/[\s\-_]+/g, '_'))); }); if (isFXVariations) { // Fix FX variations: use FieldMapper to handle all percentage field variations const fixedVariations = await Promise.all(op.value.map(async (variation) => { // Check if we need to map any percentage-related fields const mappedVariation = { key: variation.key }; // Find any field that should map to percentage_included for (const [fieldName, fieldValue] of Object.entries(variation)) { if (fieldName === 'key') continue; // Skip the key field // Check if this field should map to percentage_included const fieldMappings = await this.fieldMapper.mapFields({ [fieldName]: fieldValue }, { entityType: 'rule' }); const mapping = fieldMappings.find(m => m.templateFieldName === 'percentage_included'); if (mapping && mapping.confidence > 0.7) { this.logger.info({ variationKey: variation.key, originalField: fieldName, mappedField: 'percentage_included', value: fieldValue, confidence: mapping.confidence }, `Mapping '${fieldName}' to 'percentage_included' for FX variation`); mappedVariation.percentage_included = fieldValue; } else if (fieldName === 'percentage_included') { // Already correct field name mappedVariation.percentage_included = fieldValue; } else { // Keep other fields as-is mappedVariation[fieldName] = fieldValue; } } // Ensure we have percentage_included if (!mappedVariation.percentage_included) { this.logger.debug({ variationKey: variation.key, originalFields: Object.keys(variation) }, `No percentage field found for variation - using default 0`); mappedVariation.percentage_included = 0;