UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

219 lines 12.1 kB
/** * Analyze Data Tool - Individual Module * @description Delegates to IntelligentQueryEngine for data analysis * @since 2025-08-04 * @author Tool Modularization Team * * Migration Status: COMPLETED * Original Method: OptimizelyMCPTools.analyzeData * Complexity: HIGH * Dependencies: logger, errorMapper, storage, various analytics imports */ /** * Creates the Analyze Data tool with injected dependencies * @param deps - Injected dependencies (storage, logger, errorMapper, etc.) * @returns Tool definition with handler */ export function createAnalyzeDataTool(deps) { return { name: 'analyze_data', requiresCache: true, category: 'analytics', description: 'Perform advanced analytics queries using Micro-Kernel (ZERO-JOIN ARCHITECTURE)', handler: async (args) => { try { // Import the new Template Query Translator (DETERMINISTIC ARCHITECTURE) const { TemplateQueryTranslator } = await import('../../analytics/TemplateQueryTranslator.js'); // Create database connection const db = deps.storage.getDatabase(); if (!db) { throw new Error('Database connection unavailable. The analytics engine requires database access.'); } // Import PaginationConfigManager const { PaginationConfigManager } = await import('../../config/PaginationConfig.js'); const paginationConfig = new PaginationConfigManager(); // Only structured_query is supported now if (!args.structured_query) { return { result: 'error', message: 'structured_query parameter is required. Natural language queries are no longer supported.', error_type: 'InvalidQueryFormat', help: { example: { structured_query: { from: 'flags', select: ['flag_key', 'flag_name', 'enabled'] } }, documentation: 'Use structured_query instead of natural language query parameter' } }; } // Handle recent changes queries with special processing if (args.structured_query?.from === 'change_history_flat' && deps.handleRecentChangesQuery) { return deps.handleRecentChangesQuery(args); } // ✨ TEMPLATE QUERY TRANSLATOR - Handle structured queries deterministically deps.logger.debug({}, 'Template query: Using deterministic template-based query system'); const translator = new TemplateQueryTranslator(); try { // Use intelligent pagination extraction const { extractPagination } = await import('../../analytics/PaginationParameterExtractor.js'); const paginationInfo = extractPagination(args, { page: 1, pageSize: paginationConfig.getPageSize('analytics') }); // Merge pagination into structured query const queryWithPagination = { ...args.structured_query, page: paginationInfo.page, page_size: paginationInfo.pageSize }; deps.logger.debug({}, `Pagination: Detected format="${paginationInfo.detectedFormat}", page=${paginationInfo.page}, pageSize=${paginationInfo.pageSize}`); // Translate template to SQL const sqlResult = await translator.translateToSQL(queryWithPagination, db); deps.logger.debug({}, `Template query: Generated SQL: ${sqlResult.sql}`); // Execute COUNT query first to get total records const queryStartTime = Date.now(); deps.logger.debug({}, `Count query: ${sqlResult.countSql}`); const countResult = db.prepare(sqlResult.countSql).get(...sqlResult.params); const totalCount = countResult?.total_count || 0; // Execute main query with LIMIT/OFFSET deps.logger.debug({ params: sqlResult.params }, 'Before query: About to execute with params'); const data = db.prepare(sqlResult.sql).all(...sqlResult.params); const totalExecutionTime = Date.now() - queryStartTime; deps.logger.debug({}, `After query: Raw data type: ${typeof data}, isArray: ${Array.isArray(data)}`); const rowCount = Array.isArray(data) ? data.length : 0; deps.logger.debug({}, `Template query: Returned ${rowCount} rows (of ${totalCount} total) in ${totalExecutionTime}ms`); // Debug: Log first few rows of data if (rowCount > 0) { deps.logger.debug({ firstRow: data[0] }, 'Template query: First row sample'); } else { deps.logger.debug({ data: data }, 'Template query: No rows returned'); } // Data is already paginated by SQL LIMIT/OFFSET // Check if user wants to bypass pagination (accept multiple formats) const opts = args.options || {}; const bypassPagination = opts.bypass_pagination || opts.user_consent_required || opts.bypass_consent || opts.get_all_data || false; let finalData, pagination; if (bypassPagination) { // User wants all data - need to re-query without LIMIT deps.logger.debug({}, 'Bypass pagination: User requested all data, re-querying without LIMIT'); // Generate SQL without pagination const unpaginatedQuery = { ...args.structured_query, no_limit: true // Tell translator to skip LIMIT clause }; // Re-run query without LIMIT const allDataSql = await translator.translateToSQL(unpaginatedQuery, db); const allData = db.prepare(allDataSql.sql).all(...allDataSql.params); finalData = allData; const actualRowCount = Array.isArray(allData) ? allData.length : 0; deps.logger.debug({}, `Bypass pagination: Retrieved ALL ${actualRowCount} rows`); const consentRequired = totalCount > 50; pagination = { consent_required: false, // Already consented has_more: false, bypass_applied: true, total_count: actualRowCount, total_pages: 1, current_page: 1, page_size: actualRowCount, next_page: null, previous_page: null }; } else { // Data is already paginated by SQL - just use it const pageSize = sqlResult.limit; const page = sqlResult.page; finalData = data; // Already limited by SQL const totalPages = Math.ceil(totalCount / pageSize); const hasMore = (page * pageSize) < totalCount; // Only require consent when the requested page size exceeds 50 const consentRequired = pageSize > 50 && !bypassPagination; pagination = { consent_required: consentRequired, has_more: hasMore, total_count: totalCount, current_page: page, page_size: pageSize, total_pages: totalPages, next_page: hasMore ? page + 1 : null, previous_page: page > 1 ? page - 1 : null }; if (consentRequired && rowCount > 0) { pagination.consent_message = `This query returns ${pageSize} rows per page (${totalCount} total). Add options.bypass_pagination=true to get all results.`; } } // Generate insights if we have data let insights = null; if (finalData && finalData.length > 0) { // const { generateInsights } = await import('../../analytics/InsightGenerator.js'); const generateInsights = async (data, query, options) => { // Placeholder implementation for extracted version return { summary: `Analyzed ${data.length} records`, key_findings: [], recommendations: [] }; }; try { insights = await generateInsights(finalData, args.structured_query, { totalCount, executionTime: totalExecutionTime }); } catch (insightError) { deps.logger.warn(`Failed to generate insights: ${insightError.message}`); } } return { result: 'success', data: finalData, pagination, metadata: { query_type: 'structured', source_table: sqlResult.sourceTable, fields: sqlResult.fields, conditions: sqlResult.conditions?.length || 0, execution_time_ms: totalExecutionTime, row_count: Array.isArray(finalData) ? finalData.length : 0, total_available: totalCount }, insights, _debug: { sql: sqlResult.sql.substring(0, 200) + '...', params_count: sqlResult.params.length } }; } catch (translationError) { deps.logger.error({ error: translationError.message, stack: translationError.stack }, 'Template query translation failed'); return { result: 'error', message: translationError.message, error_type: 'QueryTranslationError', details: { structured_query: args.structured_query, error_details: translationError.details || {} } }; } } catch (error) { deps.logger.error({ error: error.message, stack: error.stack }, 'OptimizelyMCPTools.analyzeData: Failed to analyze data'); throw deps.errorMapper.toMCPError(error, 'Failed to analyze data'); } } }; } //# sourceMappingURL=AnalyzeData.js.map