@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
185 lines • 6.62 kB
JavaScript
/**
* 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