UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

670 lines 28.9 kB
/** * Template Processor Utility * @description Handles processing of entity creation templates including placeholder validation, * type checking, and data transformation for Optimizely API calls. * * Core Functions: * - Validate template completion (all {FILL:} placeholders replaced) * - Type validation against expected data types * - Extract placeholder guidance for AI agents * - Transform filled templates into API-ready payloads * * @author Optimizely MCP Server * @version 1.0.0 */ import { getLogger } from "../logging/Logger.js"; /** * Template Processor Class * Handles all template processing operations including validation and transformation */ export class TemplateProcessor { static FILL_PATTERN = /\{FILL:\s*([^}]+)\}/g; static OPTIONAL_PATTERN = /\{OPTIONAL:\s*([^}]+)\}/g; static EXISTING_PATTERN = /\{EXISTING:\s*([^}]+)\}/g; static FIND_OR_CREATE_PATTERN = /\{FIND_OR_CREATE:\s*([^}]+)\}/g; static CREATE_NEW_PATTERN = /\{CREATE_NEW:\s*([^}]+)\}/g; static PLACEHOLDER_PATTERN = /\{(FILL|OPTIONAL|EXISTING|FIND_OR_CREATE|CREATE_NEW):\s*([^}]+)\}/g; /** * Process a filled template and validate its completion * @param filledTemplate - Template with user-provided values * @param originalTemplate - Original template structure for reference * @returns Processed template with validation results */ static processTemplate(filledTemplate, originalTemplate) { const logger = getLogger(); logger.info("TemplateProcessor: Starting template processing"); // CRITICAL DEBUG: Track description field at entry if (filledTemplate && typeof filledTemplate === "object" && "description" in filledTemplate) { logger.info({ method: "processTemplate", debugPoint: "ENTRY", descriptionValue: filledTemplate.description, descriptionType: typeof filledTemplate.description, descriptionIsArray: Array.isArray(filledTemplate.description), templateKeys: Object.keys(filledTemplate), }, "DESCRIPTION BUG TRACE: TemplateProcessor.processTemplate ENTRY"); } // Extract placeholder information const placeholdersFound = this.extractPlaceholderInfo(filledTemplate); // Validate template completion const validationResult = this.validateTemplate(filledTemplate, placeholdersFound); // Process the data if validation passes const processedData = validationResult.isValid ? this.transformToAPIPayload(filledTemplate) : filledTemplate; // CRITICAL DEBUG: Track description field after transformation if (processedData && typeof processedData === "object" && "description" in processedData) { logger.info({ method: "processTemplate", debugPoint: "AFTER_TRANSFORM", descriptionValue: processedData.description, descriptionType: typeof processedData.description, descriptionIsArray: Array.isArray(processedData.description), processedKeys: Object.keys(processedData), }, "DESCRIPTION BUG TRACE: TemplateProcessor.processTemplate AFTER_TRANSFORM"); } const result = { processedData, metadata: { originalTemplate: originalTemplate || null, placeholdersFound, validationResult, }, }; logger.info(`TemplateProcessor: Processing complete. Valid: ${validationResult.isValid}, Completion: ${validationResult.completionStatus.completionPercentage}%`); return result; } /** * Extract all placeholders from a template structure * @param template - Template object to analyze * @returns Array of placeholder information */ static extractPlaceholderInfo(template) { const placeholders = []; function extractRecursive(obj, path = "") { if (typeof obj === "string") { // Check for FILL placeholders const fillMatches = Array.from(obj.matchAll(TemplateProcessor.FILL_PATTERN)); fillMatches.forEach((match) => { placeholders.push({ path: path, type: "FILL", description: match[1].trim(), currentValue: obj, expectedType: TemplateProcessor.inferTypeFromDescription(match[1]), }); }); // Check for OPTIONAL placeholders const optionalMatches = Array.from(obj.matchAll(TemplateProcessor.OPTIONAL_PATTERN)); optionalMatches.forEach((match) => { placeholders.push({ path: path, type: "OPTIONAL", description: match[1].trim(), currentValue: obj, expectedType: TemplateProcessor.inferTypeFromDescription(match[1]), }); }); // Check for EXISTING placeholders (entity must exist) const existingMatches = Array.from(obj.matchAll(TemplateProcessor.EXISTING_PATTERN)); existingMatches.forEach((match) => { const info = TemplateProcessor.parseEntityReference(match[1]); placeholders.push({ path: path, type: "EXISTING", description: match[1].trim(), currentValue: obj, entityType: info.entityType, entityIdentifier: info.identifier, }); }); // Check for FIND_OR_CREATE placeholders const findOrCreateMatches = Array.from(obj.matchAll(TemplateProcessor.FIND_OR_CREATE_PATTERN)); findOrCreateMatches.forEach((match) => { const info = TemplateProcessor.parseEntityReference(match[1]); placeholders.push({ path: path, type: "FIND_OR_CREATE", description: match[1].trim(), currentValue: obj, entityType: info.entityType, entityIdentifier: info.identifier, }); }); // Check for CREATE_NEW placeholders const createNewMatches = Array.from(obj.matchAll(TemplateProcessor.CREATE_NEW_PATTERN)); createNewMatches.forEach((match) => { const info = TemplateProcessor.parseEntityReference(match[1]); placeholders.push({ path: path, type: "CREATE_NEW", description: match[1].trim(), currentValue: obj, entityType: info.entityType, entityIdentifier: info.identifier, }); }); } else if (Array.isArray(obj)) { obj.forEach((item, index) => { extractRecursive(item, path ? `${path}[${index}]` : `[${index}]`); }); } else if (obj && typeof obj === "object") { Object.entries(obj).forEach(([key, value]) => { const newPath = path ? `${path}.${key}` : key; extractRecursive(value, newPath); }); } } extractRecursive(template); return placeholders; } /** * Validate that all required placeholders have been filled * @param template - Template to validate * @param placeholders - Extracted placeholder information * @returns Validation result with detailed feedback */ static validateTemplate(template, placeholders) { const errors = []; const warnings = []; const missingRequired = []; let totalPlaceholders = 0; let filledPlaceholders = 0; placeholders.forEach((placeholder) => { totalPlaceholders++; const hasPlaceholderSyntax = this.hasPlaceholderSyntax(placeholder.currentValue); if (hasPlaceholderSyntax) { if (placeholder.type === "FILL") { missingRequired.push(placeholder.path); errors.push(`Required field '${placeholder.path}' not filled. Expected: ${placeholder.description}`); } else { warnings.push(`Optional field '${placeholder.path}' not filled: ${placeholder.description}`); } } else { filledPlaceholders++; // Validate data type if specified const typeValidation = this.validateDataType(placeholder.currentValue, placeholder.expectedType); if (!typeValidation.isValid) { errors.push(`Invalid type for '${placeholder.path}': ${typeValidation.error}`); } } }); const completionPercentage = totalPlaceholders > 0 ? Math.round((filledPlaceholders / totalPlaceholders) * 100) : 100; const isValid = errors.length === 0 && missingRequired.length === 0; return { isValid, errors, warnings, missingRequired, completionStatus: { totalPlaceholders, filledPlaceholders, completionPercentage, }, }; } /** * Check if a value still contains placeholder syntax * @param value - Value to check * @returns True if placeholder syntax found */ static hasPlaceholderSyntax(value) { if (typeof value !== "string") return false; return this.PLACEHOLDER_PATTERN.test(value); } /** * Parse entity reference from placeholder content * Examples: "event:purchase_completed", "page:12345", "audience:mobile_users" * @param reference - Entity reference string * @returns Parsed entity type and identifier */ static parseEntityReference(reference) { const trimmed = reference.trim(); // Check for entity_type:identifier format const match = trimmed.match(/^(\w+):(.+)$/); if (match) { return { entityType: match[1], identifier: match[2].trim(), }; } // No colon format - just return as description return {}; } /** * Infer expected data type from placeholder description * @param description - Placeholder description * @returns Expected data type */ static inferTypeFromDescription(description) { const desc = description.toLowerCase(); if (desc.includes("percentage") || desc.includes("0_to_100") || desc.includes("0_to_10000")) { return "number"; } else if (desc.includes("boolean") || desc.includes("true|false")) { return "boolean"; } else if (desc.includes("array") || desc.includes("json") || desc.includes("[]")) { return "array"; } else if (desc.includes("object") || desc.includes("{}")) { return "object"; } else if (desc.includes("integer") || desc.includes("number")) { return "number"; } else { return "string"; } } /** * Validate data type against expected type * @param value - Value to validate * @param expectedType - Expected data type * @returns Validation result */ static validateDataType(value, expectedType) { if (!expectedType) return { isValid: true }; switch (expectedType) { case "number": const num = Number(value); if (isNaN(num)) { return { isValid: false, error: `Expected number, got: ${typeof value}`, }; } return { isValid: true }; case "boolean": if (typeof value === "boolean") return { isValid: true }; if (typeof value === "string" && (value === "true" || value === "false")) { return { isValid: true }; } return { isValid: false, error: `Expected boolean, got: ${typeof value}`, }; case "array": if (Array.isArray(value)) return { isValid: true }; return { isValid: false, error: `Expected array, got: ${typeof value}`, }; case "object": if (typeof value === "object" && value !== null && !Array.isArray(value)) { return { isValid: true }; } return { isValid: false, error: `Expected object, got: ${typeof value}`, }; case "string": if (typeof value === "string") return { isValid: true }; return { isValid: false, error: `Expected string, got: ${typeof value}`, }; default: return { isValid: true }; } } /** * Transform filled template into API-ready payload * @param template - Filled template * @returns API-ready data structure */ static transformToAPIPayload(template) { // CRITICAL DEBUG: Track description field at entry if (template && typeof template === "object" && "description" in template) { getLogger().info({ method: "transformToAPIPayload", debugPoint: "ENTRY", descriptionValue: template.description, descriptionType: typeof template.description, descriptionIsArray: Array.isArray(template.description), templateKeys: Object.keys(template), }, "DESCRIPTION BUG TRACE: transformToAPIPayload ENTRY"); } // Deep clone to avoid modifying original const payload = JSON.parse(JSON.stringify(template)); // CRITICAL DEBUG: Track description field after clone if (payload && typeof payload === "object" && "description" in payload) { getLogger().info({ method: "transformToAPIPayload", debugPoint: "AFTER_CLONE", descriptionValue: payload.description, descriptionType: typeof payload.description, descriptionIsArray: Array.isArray(payload.description), payloadKeys: Object.keys(payload), }, "DESCRIPTION BUG TRACE: transformToAPIPayload AFTER_CLONE"); } // Clean up unfilled optional sections this.cleanUnfilledSections(payload); // CRITICAL DEBUG: Track description field after clean if (payload && typeof payload === "object" && "description" in payload) { getLogger().info({ method: "transformToAPIPayload", debugPoint: "AFTER_CLEAN", descriptionValue: payload.description, descriptionType: typeof payload.description, descriptionIsArray: Array.isArray(payload.description), payloadKeys: Object.keys(payload), }, "DESCRIPTION BUG TRACE: transformToAPIPayload AFTER_CLEAN"); } // Transform specific field formats for API compatibility this.transformAPIFields(payload); // CRITICAL DEBUG: Track description field after transform if (payload && typeof payload === "object" && "description" in payload) { getLogger().info({ method: "transformToAPIPayload", debugPoint: "AFTER_TRANSFORM_FIELDS", descriptionValue: payload.description, descriptionType: typeof payload.description, descriptionIsArray: Array.isArray(payload.description), payloadKeys: Object.keys(payload), }, "DESCRIPTION BUG TRACE: transformToAPIPayload AFTER_TRANSFORM_FIELDS"); } return payload; } /** * Remove sections that still contain unfilled placeholders * @param obj - Object to clean */ static cleanUnfilledSections(obj) { if (!obj || typeof obj !== "object") return; // Process arrays if (Array.isArray(obj)) { // Filter out array items that contain placeholder syntax for (let i = obj.length - 1; i >= 0; i--) { if (this.containsPlaceholderSyntax(obj[i])) { obj.splice(i, 1); } else { this.cleanUnfilledSections(obj[i]); } } return; } // Process objects const keysToDelete = []; Object.entries(obj).forEach(([key, value]) => { if (this.containsPlaceholderSyntax(value)) { keysToDelete.push(key); } else if (value && typeof value === "object") { this.cleanUnfilledSections(value); // After cleaning, check if object/array is now empty if (Array.isArray(value) && value.length === 0) { keysToDelete.push(key); } else if (!Array.isArray(value) && Object.keys(value).length === 0) { keysToDelete.push(key); } } }); // Remove marked keys keysToDelete.forEach((key) => delete obj[key]); } /** * Check if a value contains placeholder syntax (recursively) * @param value - Value to check * @returns True if placeholder syntax is found */ static containsPlaceholderSyntax(value) { if (typeof value === "string") { return this.PLACEHOLDER_PATTERN.test(value); } else if (Array.isArray(value)) { return value.some((item) => this.containsPlaceholderSyntax(item)); } else if (value && typeof value === "object") { return Object.values(value).some((v) => this.containsPlaceholderSyntax(v)); } return false; } /** * Transform fields to match API expectations * @param payload - Payload to transform */ static transformAPIFields(payload) { function transformRecursive(obj, path = "") { if (Array.isArray(obj)) { obj.forEach((item, index) => transformRecursive(item, `${path}[${index}]`)); } else if (obj && typeof obj === "object") { Object.entries(obj).forEach(([key, value]) => { const currentPath = path ? `${path}.${key}` : key; // Convert percentage strings to numbers if (typeof value === "string" && /^\d+$/.test(value) && (key.includes("percentage") || key.includes("weight") || key.includes("allocation"))) { obj[key] = parseInt(value, 10); } // Convert boolean strings to booleans if (typeof value === "string" && (value === "true" || value === "false")) { obj[key] = value === "true"; } // Special handling for page conditions - ensure it's a string if (key === "conditions" && currentPath.includes("page")) { if (value && typeof value !== "string") { obj[key] = JSON.stringify(value); getLogger().info({ path: currentPath, original: value, stringified: obj[key], }, "TemplateProcessor: Stringified page conditions field"); } } // Recursively process nested objects if (typeof value === "object") { transformRecursive(value, currentPath); } }); } } transformRecursive(payload); } /** * Get guidance for AI agents on what information to collect * @param template - Original template structure * @returns Array of guidance instructions for collecting user input */ static getCollectionGuidance(template) { const placeholders = this.extractPlaceholderInfo(template); const guidance = []; // Group by requirement type const required = placeholders.filter((p) => p.type === "FILL"); const optional = placeholders.filter((p) => p.type === "OPTIONAL"); if (required.length > 0) { guidance.push("Required information to collect from user:"); required.forEach((p) => { guidance.push(` • ${p.path}: ${p.description}`); }); } if (optional.length > 0) { guidance.push("Optional information you can ask for:"); optional.forEach((p) => { guidance.push(` • ${p.path}: ${p.description}`); }); } guidance.push(""); guidance.push("Instructions:"); guidance.push("1. Ask user for required information first"); guidance.push("2. Offer to collect optional information"); guidance.push("3. Replace {FILL: ...} placeholders with user values"); guidance.push("4. Leave {OPTIONAL: ...} placeholders if user skips them"); guidance.push('5. Call manage_entity_lifecycle with mode="template"'); return guidance; } /** * Generate example filled template for AI agent reference * @param template - Original template * @param entityType - Type of entity being created * @returns Example with realistic values */ static generateExampleTemplate(template, entityType) { const example = JSON.parse(JSON.stringify(template)); function fillExample(obj, path = "") { if (typeof obj === "string") { // Replace FILL placeholders with example values obj = obj.replace(TemplateProcessor.FILL_PATTERN, (match, description) => { return TemplateProcessor.getExampleValue(description.trim(), path, entityType); }); // Remove OPTIONAL placeholders (leave them unfilled) obj = obj.replace(TemplateProcessor.OPTIONAL_PATTERN, ""); return obj; } else if (Array.isArray(obj)) { for (let i = 0; i < obj.length; i++) { obj[i] = fillExample(obj[i], `${path}[${i}]`); } } else if (obj && typeof obj === "object") { Object.keys(obj).forEach((key) => { const newPath = path ? `${path}.${key}` : key; obj[key] = fillExample(obj[key], newPath); }); } return obj; } return fillExample(example); } /** * Process entity reference placeholders in template * This transforms entity references into their resolved IDs/keys * @param template - Template with entity references * @param entityResolver - Function to resolve entity references * @returns Template with resolved references */ static async resolveEntityReferences(template, entityResolver) { const errors = []; const resolved = JSON.parse(JSON.stringify(template)); // Deep clone async function resolveRecursive(obj, path = "") { if (typeof obj === "string") { // Check for EXISTING references const existingMatch = obj.match(/^\{EXISTING:\s*([^}]+)\}$/); if (existingMatch) { const info = TemplateProcessor.parseEntityReference(existingMatch[1]); if (info.entityType && info.identifier) { const resolvedId = await entityResolver(info.entityType, info.identifier, "existing"); if (resolvedId) { return resolvedId; } else { errors.push(`${path}: Required entity not found: ${existingMatch[1]}`); return obj; // Keep original for error tracking } } } // Check for FIND_OR_CREATE references const findOrCreateMatch = obj.match(/^\{FIND_OR_CREATE:\s*([^}]+)\}$/); if (findOrCreateMatch) { const info = TemplateProcessor.parseEntityReference(findOrCreateMatch[1]); if (info.entityType && info.identifier) { const resolvedId = await entityResolver(info.entityType, info.identifier, "find_or_create"); return resolvedId || obj; } } // Check for CREATE_NEW references const createNewMatch = obj.match(/^\{CREATE_NEW:\s*([^}]+)\}$/); if (createNewMatch) { const info = TemplateProcessor.parseEntityReference(createNewMatch[1]); if (info.entityType && info.identifier) { const resolvedId = await entityResolver(info.entityType, info.identifier, "create_new"); return resolvedId || obj; } } return obj; } else if (Array.isArray(obj)) { for (let i = 0; i < obj.length; i++) { obj[i] = await resolveRecursive(obj[i], `${path}[${i}]`); } return obj; } else if (obj && typeof obj === "object") { for (const key of Object.keys(obj)) { const newPath = path ? `${path}.${key}` : key; obj[key] = await resolveRecursive(obj[key], newPath); } return obj; } return obj; } await resolveRecursive(resolved); return { resolved, errors }; } /** * Generate example values for placeholders * @param description - Placeholder description * @param path - Field path * @param entityType - Entity type for context * @returns Example value */ static getExampleValue(description, path, entityType) { const desc = description.toLowerCase(); // Type-specific examples if (desc.includes("percentage") || desc.includes("0_to_100")) { return "50"; } else if (desc.includes("0_to_10000")) { return "5000"; } else if (desc.includes("true|false") || desc.includes("boolean")) { return "true"; } else if (desc.includes("unique") && desc.includes("key")) { return `${entityType}_test_${Date.now()}`; } else if (path.includes("name") || desc.includes("display_name")) { return `Test ${entityType.charAt(0).toUpperCase() + entityType.slice(1)}`; } else if (desc.includes("url")) { return "https://example.com"; } else if (desc.includes("email")) { return "user@example.com"; } else if (desc.includes("event_key")) { return "custom_event"; } else if (desc.includes("environment")) { return "development"; } else if (desc.includes("variation") && desc.includes("key")) { return "control"; } else if (desc.includes("variation") && desc.includes("name")) { return "Control"; } // Default examples based on common patterns return `example_${path.split(".").pop() || "value"}`; } } //# sourceMappingURL=TemplateProcessor.js.map