UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

185 lines 6.62 kB
/** * QueryDecomposer - Breaks natural language queries into atomic intents * Part of Day 1 implementation from prescriptive plan */ import { getLogger } from '../../../logging/Logger.js'; export class QueryDecomposer { logger = getLogger(); intentIdCounter = 0; /** * Main decomposition method - transforms query into atomic intents */ decompose(query) { const normalizedQuery = this.normalizeQuery(query); const queryType = this.determineQueryType(normalizedQuery); const intents = this.extractAtomicIntents(normalizedQuery, queryType); return { originalQuery: query, normalizedQuery, queryType, atomicIntents: intents, confidence: this.calculateConfidence(intents) }; } /** * Determine the primary query type based on keywords and patterns */ determineQueryType(query) { // CRITICAL L6-6 FIX: Check for "show me the number of" pattern first if (/^(show|give)\s+me\s+the\s+(number|count|total)\s+of/i.test(query)) { // "Show me the number of" is a COUNT query, not a list return 'count'; } // CRITICAL FIX: Check for LIST/SHOW first to handle "List all X with how many Y" patterns // This prevents misclassification of list queries that include count aggregations if (/^(list|show|display|get|fetch|retrieve|provide)\b/i.test(query)) { // Even if query contains "how many", if it starts with list/show, it's a list query return 'list'; } // Now check for pure COUNT queries (total counts, not lists with counts) if (/\b(count|how many|number of|total number)\b/i.test(query)) { return 'count'; } if (/\b(sum|total|aggregate|add up)\b/i.test(query)) { return 'sum'; } if (/\b(compare|versus|vs|difference between)\b/i.test(query)) { return 'compare'; } if (/\b(analyze|analysis|breakdown|insights?)\b/i.test(query)) { return 'analyze'; } if (/\b(detail|details|full|complete|all information)\b/i.test(query)) { return 'detail'; } // Default to list for show/list/get queries return 'list'; } /** * Extract atomic intents from the query */ extractAtomicIntents(query, queryType) { const intents = []; let intentId = this.intentIdCounter++; // Extract action intent const actionKeywords = this.extractActionKeywords(query); if (actionKeywords.length > 0) { intents.push({ id: `action_${intentId++}`, type: 'action', value: { action: queryType, keywords: actionKeywords }, confidence: 0.9, dependencies: [], metadata: { position: 0, rawText: actionKeywords.join(' '), alternatives: [] } }); } // Extract entity intents const entities = this.extractEntities(query); entities.forEach(entity => { intents.push({ id: `entity_${intentId++}`, type: 'entity', value: entity, confidence: entity.confidence || 0.8, dependencies: [], metadata: { position: entity.position || 0, rawText: entity.match || '', alternatives: [] } }); }); // Extract filter intents const filters = this.extractFilters(query); filters.forEach(filter => { intents.push({ id: `filter_${intentId++}`, type: 'filter', value: filter, confidence: 0.8, dependencies: [], metadata: { position: 0, rawText: filter.rawText || '', alternatives: [] } }); }); // Extract aggregation intents (GROUP BY, etc.) if (queryType === 'count' || queryType === 'sum') { const groupBy = this.extractGroupBy(query); if (groupBy) { intents.push({ id: `aggregation_${intentId++}`, type: 'aggregation', value: { type: 'groupBy', field: groupBy }, confidence: 0.85, dependencies: [], metadata: { position: 0, rawText: groupBy, alternatives: [] } }); } } return intents; } // Helper methods - implement based on existing patterns normalizeQuery(query) { return query.toLowerCase().trim().replace(/\s+/g, ' '); } extractActionKeywords(query) { const keywords = []; const actionPatterns = [ /\b(count|show|list|get|find|give|display)\b/gi, /\b(sum|total|average|analyze)\b/gi ]; actionPatterns.forEach(pattern => { const matches = query.match(pattern); if (matches) keywords.push(...matches); }); return keywords; } extractEntities(query) { // TODO: Use existing entity extraction logic return []; } extractFilters(query) { // TODO: Use existing filter extraction logic return []; } extractGroupBy(query) { const groupByPattern = /\b(?:group(?:ed)?|breakdown|segment(?:ed)?)\s+by\s+(\w+)/i; const perPattern = /\bper\s+(\w+)/i; const byPattern = /\bby\s+(\w+)/i; let match = query.match(groupByPattern); if (match) return match[1]; match = query.match(perPattern); if (match) return match[1]; match = query.match(byPattern); if (match) return match[1]; return null; } calculateConfidence(intents) { if (intents.length === 0) return 0; const avgConfidence = intents.reduce((sum, i) => sum + i.confidence, 0) / intents.length; return Math.min(avgConfidence * 1.1, 1.0); // Boost slightly for multiple intents } } //# sourceMappingURL=QueryDecomposer.js.map