UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

459 lines 15.6 kB
/** * Query Normalizer for Intelligent Query Caching * * Converts natural language queries and variations into a standardized format * for consistent cache key generation and semantic understanding. */ import { getLogger } from '../../../logging/Logger.js'; export class QueryNormalizer { logger = getLogger(); // Common query patterns and their normalized forms queryPatterns = [ // List operations { pattern: /^(show|list|get|display|find)?\s*(?:all\s+)?(\w+)s?$/i, operation: 'list' }, { pattern: /^(how many|count|total)\s+(\w+)s?/i, operation: 'count' }, { pattern: /^(analyze|analysis|breakdown)\s+(\w+)s?/i, operation: 'analyze' }, { pattern: /^(compare|diff|difference)\s+(\w+)s?/i, operation: 'compare' }, { pattern: /^(\w+)\s+details?\s+(?:for\s+)?(.+)$/i, operation: 'detail' }, ]; // Entity synonyms mapping entitySynonyms = { 'flags': 'flag', 'feature flags': 'flag', 'feature flag': 'flag', 'experiments': 'experiment', 'campaigns': 'campaign', 'audiences': 'audience', 'users': 'audience', 'events': 'event', 'metrics': 'event', 'attributes': 'attribute', 'projects': 'project', 'environments': 'environment', 'envs': 'environment', }; // Time range patterns timePatterns = [ { pattern: /last\s+(\d+)\s+(days?|weeks?|months?)/i, type: 'relative' }, { pattern: /past\s+(\d+)\s+(days?|weeks?|months?)/i, type: 'relative' }, { pattern: /today/i, type: 'relative', duration: 'today' }, { pattern: /yesterday/i, type: 'relative', duration: 'yesterday' }, { pattern: /this\s+(week|month|year)/i, type: 'relative' }, { pattern: /since\s+(.+)/i, type: 'absolute' }, { pattern: /between\s+(.+)\s+and\s+(.+)/i, type: 'absolute' }, ]; /** * Normalize a query from natural language or structured format */ normalize(query) { if (typeof query === 'string') { return this.normalizeNaturalLanguage(query); } else if (query.find) { // Handle UniversalQuery format return this.normalizeUniversalQuery(query); } else { return this.normalizeStructuredQuery(query); } } /** * Normalize a natural language query */ normalizeNaturalLanguage(query) { this.logger.debug({ query }, 'Normalizing natural language query'); const normalized = { entity: '', operation: 'list', filters: {}, projections: [], joins: [], aggregations: [], }; // Clean and standardize the query const cleanQuery = query.toLowerCase().trim(); // Extract entity normalized.entity = this.extractEntity(cleanQuery); // Extract operation normalized.operation = this.extractOperation(cleanQuery); // Extract filters normalized.filters = this.extractFilters(cleanQuery); // Extract time range const timeRange = this.extractTimeRange(cleanQuery); if (timeRange) { normalized.timeRange = timeRange; } // Extract aggregations normalized.aggregations = this.extractAggregations(cleanQuery); // Extract grouping const groupBy = this.extractGroupBy(cleanQuery); if (groupBy.length > 0) { normalized.groupBy = groupBy; } // Extract joins from context normalized.joins = this.extractImplicitJoins(cleanQuery, normalized.entity); // Extract limit const limit = this.extractLimit(cleanQuery); if (limit) { normalized.limit = limit; } this.logger.debug({ normalized }, 'Query normalization complete'); return normalized; } /** * Normalize a UniversalQuery format */ normalizeUniversalQuery(query) { const normalized = { entity: query.find, operation: query.select?.length ? 'select' : 'list', filters: {}, projections: query.select || [], joins: query.join?.map((j) => j.with) || [], aggregations: query.aggregations?.map((a) => a.function.toUpperCase()) || [], }; // Convert where conditions to filters object if (query.where && Array.isArray(query.where)) { query.where.forEach((condition) => { normalized.filters[condition.field] = condition.value; }); } if (query.groupBy) { normalized.groupBy = query.groupBy; } if (query.orderBy) { normalized.orderBy = query.orderBy.map((o) => ({ field: o.field, direction: o.direction.toLowerCase() })); } if (query.limit) { normalized.limit = query.limit; } return normalized; } /** * Normalize a structured query */ normalizeStructuredQuery(query) { const normalized = { entity: query.from, operation: query.select?.length ? 'select' : 'list', filters: query.where || {}, projections: query.select || [], joins: query.joins?.map(j => j.entity) || [], aggregations: this.extractAggregationsFromProjections(query.select || []), }; if (query.groupBy) { normalized.groupBy = query.groupBy; } if (query.orderBy) { normalized.orderBy = query.orderBy; } if (query.limit) { normalized.limit = query.limit; } return normalized; } /** * Extract entity type from query */ extractEntity(query) { // Check each synonym pattern for (const [synonym, entity] of Object.entries(this.entitySynonyms)) { if (query.includes(synonym)) { return entity; } } // Try to find entity name in common patterns const entityMatch = query.match(/\b(flag|experiment|campaign|audience|event|attribute|project|environment|variation|rule)\b/); if (entityMatch) { return entityMatch[1]; } // Default to flag if unclear return 'flag'; } /** * Extract operation type from query */ extractOperation(query) { // Check for aggregation keywords first if (/\b(count|how many|total)\b/i.test(query)) { return 'count'; } if (/\b(analyze|analysis|breakdown|distribution)\b/i.test(query)) { return 'analyze'; } if (/\b(compare|diff|difference|versus)\b/i.test(query)) { return 'compare'; } if (/\b(detail|details|info|information|specific)\b/i.test(query)) { return 'detail'; } if (/\b(group|grouped|by)\b/i.test(query)) { return 'aggregate'; } // Default to list return 'list'; } /** * Extract filters from query */ extractFilters(query) { const filters = {}; // Status filters if (/\benabled\b/.test(query)) { filters.enabled = true; } if (/\bdisabled\b/.test(query)) { filters.enabled = false; } if (/\barchived\b/.test(query)) { filters.archived = true; } if (/\bactive\b/.test(query)) { filters.archived = false; } // Environment filters const envMatch = query.match(/\bin\s+(production|development|staging|dev|prod|stage)\b/); if (envMatch) { filters.environment = envMatch[1].replace('prod', 'production').replace('dev', 'development'); } // Platform filters if (/\bweb\s+platform\b/.test(query)) { filters.platform = 'web'; } if (/\bfeature\s+(?:experimentation|flags?)\b/.test(query)) { filters.platform = 'feature'; } // Status filters for experiments if (/\brunning\b/.test(query)) { filters.status = 'running'; } if (/\bpaused\b/.test(query)) { filters.status = 'paused'; } if (/\bcompleted\b/.test(query)) { filters.status = 'completed'; } return filters; } /** * Extract time range from query */ extractTimeRange(query) { // Check relative patterns for (const pattern of this.timePatterns) { const match = query.match(pattern.pattern); if (match) { if (pattern.type === 'relative') { if (pattern.duration) { return { type: 'relative', duration: pattern.duration }; } else if (match[1] && match[2]) { return { type: 'relative', duration: `last_${match[1]}_${match[2]}` }; } else if (match[1]) { return { type: 'relative', duration: `this_${match[1]}` }; } } else if (pattern.type === 'absolute') { if (match[2]) { // Between X and Y return { type: 'absolute', start: match[1], end: match[2] }; } else if (match[1]) { // Since X return { type: 'absolute', start: match[1] }; } } } } // Check for specific date patterns const dateMatch = query.match(/\b(\d{4}-\d{2}-\d{2})\b/); if (dateMatch) { return { type: 'absolute', start: dateMatch[1], end: dateMatch[1] }; } return undefined; } /** * Extract aggregation functions */ extractAggregations(query) { const aggregations = []; if (/\bcount\b/i.test(query)) { aggregations.push('COUNT'); } if (/\b(sum|total)\b/i.test(query)) { aggregations.push('SUM'); } if (/\b(avg|average)\b/i.test(query)) { aggregations.push('AVG'); } if (/\b(min|minimum)\b/i.test(query)) { aggregations.push('MIN'); } if (/\b(max|maximum)\b/i.test(query)) { aggregations.push('MAX'); } return aggregations; } /** * Extract aggregations from projection fields */ extractAggregationsFromProjections(projections) { const aggregations = []; const aggPattern = /^(COUNT|SUM|AVG|MIN|MAX)\(/i; for (const projection of projections) { const match = projection.match(aggPattern); if (match) { aggregations.push(match[1].toUpperCase()); } } return aggregations; } /** * Extract group by fields */ extractGroupBy(query) { const groupBy = []; // Look for "by X" or "group by X" patterns const groupMatch = query.match(/(?:group\s+)?by\s+(\w+)(?:\s+and\s+(\w+))?/i); if (groupMatch) { groupBy.push(groupMatch[1]); if (groupMatch[2]) { groupBy.push(groupMatch[2]); } } return groupBy; } /** * Extract implicit joins based on query context */ extractImplicitJoins(query, primaryEntity) { const joins = []; // Flag queries mentioning experiments if (primaryEntity === 'flag' && /\bexperiment/i.test(query)) { joins.push('experiment'); } // Experiment queries mentioning metrics/events if (primaryEntity === 'experiment' && /\b(metric|event|conversion)/i.test(query)) { joins.push('event'); } // Queries mentioning audiences if (/\baudience|targeting/i.test(query) && !joins.includes('audience')) { joins.push('audience'); } // Queries mentioning projects if (/\bproject/i.test(query) && primaryEntity !== 'project') { joins.push('project'); } return joins; } /** * Extract limit from query */ extractLimit(query) { const limitMatch = query.match(/(?:top|first|limit)\s+(\d+)/i); if (limitMatch) { return parseInt(limitMatch[1], 10); } // Check for "show X" pattern const showMatch = query.match(/show\s+(\d+)\s+/i); if (showMatch) { return parseInt(showMatch[1], 10); } return undefined; } /** * Check if two normalized queries are semantically equivalent */ areEquivalent(query1, query2) { // Must be same entity and operation if (query1.entity !== query2.entity || query1.operation !== query2.operation) { return false; } // Compare filters (order doesn't matter) if (!this.objectsEqual(query1.filters, query2.filters)) { return false; } // Compare arrays (order matters for some, not for others) if (!this.arraysEqual(query1.projections, query2.projections)) { return false; } if (!this.arraysEqual(query1.joins, query2.joins, false)) { return false; } if (!this.arraysEqual(query1.aggregations, query2.aggregations, false)) { return false; } // Compare optional fields if (query1.limit !== query2.limit) { return false; } // Time ranges need special comparison if (!this.timeRangesEqual(query1.timeRange, query2.timeRange)) { return false; } return true; } objectsEqual(obj1, obj2) { const keys1 = Object.keys(obj1); const keys2 = Object.keys(obj2); if (keys1.length !== keys2.length) { return false; } for (const key of keys1) { if (obj1[key] !== obj2[key]) { return false; } } return true; } arraysEqual(arr1, arr2, orderMatters = true) { if (arr1.length !== arr2.length) { return false; } if (orderMatters) { return arr1.every((val, idx) => val === arr2[idx]); } else { // For unordered comparison const sorted1 = [...arr1].sort(); const sorted2 = [...arr2].sort(); return sorted1.every((val, idx) => val === sorted2[idx]); } } timeRangesEqual(tr1, tr2) { if (!tr1 && !tr2) return true; if (!tr1 || !tr2) return false; if (tr1.type !== tr2.type) return false; if (tr1.type === 'relative') { return tr1.duration === tr2.duration; } else { return tr1.start === tr2.start && tr1.end === tr2.end; } } } //# sourceMappingURL=QueryNormalizer.js.map