From 71cbb570ef935906e8d28cb73328779c8772d060 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Tue, 3 Mar 2026 11:53:59 -0800 Subject: [PATCH] feat(ai-diagnostics): add query parser and executor [3.1-3.2] --- .../modules/ai-diagnostics/query-executor.ts | 502 ++++++++++++++++++ .../modules/ai-diagnostics/query-parser.ts | 2 +- 2 files changed, 503 insertions(+), 1 deletion(-) create mode 100644 services/platform-service/src/modules/ai-diagnostics/query-executor.ts diff --git a/services/platform-service/src/modules/ai-diagnostics/query-executor.ts b/services/platform-service/src/modules/ai-diagnostics/query-executor.ts new file mode 100644 index 00000000..81d9ce4d --- /dev/null +++ b/services/platform-service/src/modules/ai-diagnostics/query-executor.ts @@ -0,0 +1,502 @@ +import type { ParsedQuery } from './query-parser.js'; +import type { QueryIntent, ExtractedEntities, DiagnosticInsightDoc, ErrorClusterDoc } from './types.js'; +import * as repository from './repository.js'; +import { analyzeRootCause, generatePatternSummary } from './llm-analyzer.js'; +import { aggregateClusterContext } from './telemetry-linking.js'; + +// ============================================================================ +// Query Executor +// ============================================================================ + +export interface QueryExecutionResult { + query: string; + intent: QueryIntent; + aiResponse: string; + confidence: number; + supportingData: Array<{ + type: 'cluster' | 'insight' | 'trend' | 'comparison'; + id: string; + title: string; + relevanceScore: number; + data: unknown; + }>; + suggestedActions: string[]; + executionTimeMs: number; +} + +interface ExecutionContext { + productId?: string; + userId: string; +} + +/** + * Executes a parsed query and generates results + */ +export async function executeQuery( + parsedQuery: ParsedQuery, + context: ExecutionContext +): Promise { + const startTime = Date.now(); + + // Route to appropriate handler based on intent + switch (parsedQuery.intent) { + case 'root_cause': + return await executeRootCauseQuery(parsedQuery, context, startTime); + case 'pattern_search': + return await executePatternSearchQuery(parsedQuery, context, startTime); + case 'comparison': + return await executeComparisonQuery(parsedQuery, context, startTime); + case 'trend': + return await executeTrendQuery(parsedQuery, context, startTime); + case 'impact': + return await executeImpactQuery(parsedQuery, context, startTime); + default: + return await executeGenericQuery(parsedQuery, context, startTime); + } +} + +// ============================================================================ +// Intent-Specific Executors +// ============================================================================ + +async function executeRootCauseQuery( + parsedQuery: ParsedQuery, + context: ExecutionContext, + startTime: number +): Promise { + const { entities } = parsedQuery; + const productId = context.productId || entities.products?.[0]; + + if (!productId) { + return createErrorResult(parsedQuery, startTime, 'No product specified'); + } + + // Find relevant clusters + const clusters = await repository.findClustersByProduct(productId, { + status: 'active', + limit: 10, + }); + + // Filter by error type if specified + let relevantClusters = clusters; + if (entities.errorTypes?.length) { + relevantClusters = clusters.filter((c) => + entities.errorTypes?.some((type) => + c.errorType.toLowerCase().includes(type.toLowerCase()) + ) + ); + } + + if (relevantClusters.length === 0) { + return createEmptyResult(parsedQuery, startTime, 'No matching error clusters found'); + } + + // Get the most frequent cluster + const topCluster = relevantClusters[0]; + + // Check for existing insight + const existingInsight = await repository.getLatestInsightForCluster( + topCluster.id, + productId + ); + + let aiResponse: string; + let supportingData: QueryExecutionResult['supportingData'] = []; + + if (existingInsight) { + aiResponse = formatInsightResponse(existingInsight); + supportingData.push({ + type: 'insight', + id: existingInsight.id, + title: 'Pre-computed Analysis', + relevanceScore: 0.9, + data: existingInsight, + }); + } else { + // Generate new analysis + const contextSummary = await aggregateClusterContext(topCluster, []); + const analysis = await analyzeRootCause({ + cluster: topCluster, + context: contextSummary, + sampleStackTraces: [topCluster.stackSignature], + relatedClusters: [], + analysisType: 'root_cause', + }); + + aiResponse = analysis.hypothesis || 'Analysis pending additional data.'; + supportingData.push({ + type: 'cluster', + id: topCluster.id, + title: `${topCluster.errorType}: ${topCluster.occurrenceCount} occurrences`, + relevanceScore: 1.0, + data: topCluster, + }); + } + + // Add related clusters + for (const cluster of relevantClusters.slice(1, 4)) { + supportingData.push({ + type: 'cluster', + id: cluster.id, + title: `${cluster.errorType}: ${cluster.occurrenceCount} occurrences`, + relevanceScore: 0.7, + data: cluster, + }); + } + + return { + query: parsedQuery.rawQuery, + intent: 'root_cause', + aiResponse, + confidence: existingInsight?.confidenceScore || 0.6, + supportingData, + suggestedActions: existingInsight?.suggestedInvestigation || [ + 'View cluster details', + 'Check related errors', + 'Review telemetry traces', + ], + executionTimeMs: Date.now() - startTime, + }; +} + +async function executePatternSearchQuery( + parsedQuery: ParsedQuery, + context: ExecutionContext, + startTime: number +): Promise { + const { entities } = parsedQuery; + const productId = context.productId || entities.products?.[0]; + + if (!productId) { + return createErrorResult(parsedQuery, startTime, 'No product specified'); + } + + // Search clusters + const clusters = await repository.findClustersByProduct(productId, { + limit: 20, + }); + + // Filter by criteria + let results = clusters; + + if (entities.errorTypes?.length) { + results = results.filter((c) => + entities.errorTypes?.some((type) => + c.errorType.toLowerCase().includes(type.toLowerCase()) + ) + ); + } + + if (entities.platforms?.length) { + results = results.filter((c) => + c.commonContext?.osVersions?.some((os) => + entities.platforms?.some((p) => os.version.toLowerCase().includes(p.toLowerCase())) + ) + ); + } + + // Generate summaries + const summaries = await Promise.all( + results.slice(0, 5).map(async (cluster) => { + const summary = await generatePatternSummary(cluster, { + totalOccurrences: cluster.occurrenceCount, + affectedUsers: [], + timeRange: { start: cluster.firstSeenAt, end: cluster.lastSeenAt }, + mostCommonScreens: cluster.commonContext?.screenContexts || [], + mostCommonActions: [], + featureFlagCorrelations: [], + }); + return { cluster, summary }; + }) + ); + + const aiResponse = summaries.length > 0 + ? `Found ${results.length} matching error clusters:\n\n` + + summaries.map((s, i) => `${i + 1}. **${s.cluster.errorType}** (${s.cluster.occurrenceCount} occurrences)\n ${s.summary}`).join('\n\n') + : 'No matching error patterns found.'; + + return { + query: parsedQuery.rawQuery, + intent: 'pattern_search', + aiResponse, + confidence: results.length > 0 ? 0.8 : 0.3, + supportingData: results.slice(0, 5).map((cluster) => ({ + type: 'cluster', + id: cluster.id, + title: cluster.errorType, + relevanceScore: 0.8, + data: cluster, + })), + suggestedActions: [ + 'View cluster details', + 'Compare with historical data', + 'Filter by additional criteria', + ], + executionTimeMs: Date.now() - startTime, + }; +} + +async function executeComparisonQuery( + parsedQuery: ParsedQuery, + context: ExecutionContext, + startTime: number +): Promise { + const { entities } = parsedQuery; + const productId = context.productId || entities.products?.[0]; + + if (!productId) { + return createErrorResult(parsedQuery, startTime, 'No product specified'); + } + + // Get recent clusters for comparison + const clusters = await repository.findClustersByProduct(productId, { + limit: 10, + }); + + if (clusters.length < 2) { + return createEmptyResult(parsedQuery, startTime, 'Not enough clusters for comparison'); + } + + const clusterA = clusters[0]; + const clusterB = clusters[1]; + + const aiResponse = `Comparing top 2 error clusters: + +**${clusterA.errorType}** (${clusterA.occurrenceCount} occurrences) +- First seen: ${clusterA.firstSeenAt} +- Status: ${clusterA.status} +- Message pattern: ${clusterA.messageTemplate.slice(0, 100)}... + +**${clusterB.errorType}** (${clusterB.occurrenceCount} occurrences) +- First seen: ${clusterB.firstSeenAt} +- Status: ${clusterB.status} +- Message pattern: ${clusterB.messageTemplate.slice(0, 100)}... + +Key differences: +- ${clusterA.errorType !== clusterB.errorType ? 'Different error types' : 'Same error type'} +- ${Math.abs(clusterA.occurrenceCount - clusterB.occurrenceCount) > 10 ? 'Significant difference in occurrence count' : 'Similar occurrence rates'} +- ${clusterA.status !== clusterB.status ? 'Different resolution status' : 'Same status'}`; + + return { + query: parsedQuery.rawQuery, + intent: 'comparison', + aiResponse, + confidence: 0.7, + supportingData: [ + { + type: 'comparison', + id: `${clusterA.id}_vs_${clusterB.id}`, + title: 'Cluster Comparison', + relevanceScore: 1.0, + data: { clusterA, clusterB }, + }, + ], + suggestedActions: [ + 'View detailed comparison', + 'Compare with additional clusters', + 'Analyze temporal patterns', + ], + executionTimeMs: Date.now() - startTime, + }; +} + +async function executeTrendQuery( + parsedQuery: ParsedQuery, + context: ExecutionContext, + startTime: number +): Promise { + const { entities } = parsedQuery; + const productId = context.productId || entities.products?.[0]; + + if (!productId) { + return createErrorResult(parsedQuery, startTime, 'No product specified'); + } + + const timeRange = entities.timeRange || { + start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), + end: new Date().toISOString(), + }; + + // Get trends + const trends = await repository.getClusterTrends(productId, { + start: timeRange.start, + end: timeRange.end, + }); + + const totalErrors = trends.reduce((sum, t) => sum + t.occurrenceCount, 0); + const uniqueErrorTypes = new Set(trends.map((t) => t.errorType)).size; + + const aiResponse = `Error trends for ${productId} (${timeRange.start.slice(0, 10)} to ${timeRange.end.slice(0, 10)}): + +**Summary:** +- Total errors: ${totalErrors} +- Unique error types: ${uniqueErrorTypes} +- Most affected clusters: ${trends.slice(0, 3).map((t) => t.errorType).join(', ') || 'N/A'} + +**Top Clusters:** +${trends.slice(0, 5).map((t, i) => `${i + 1}. ${t.errorType}: ${t.occurrenceCount} occurrences (${t.uniqueUsers} users)`).join('\n') || 'No data available'} + +${trends.length > 0 && trends[0].occurrenceCount > trends[trends.length - 1]?.occurrenceCount * 2 ? '⚠️ Some errors are significantly more frequent than others.' : '✓ Error distribution appears balanced.'}`; + + return { + query: parsedQuery.rawQuery, + intent: 'trend', + aiResponse, + confidence: 0.75, + supportingData: trends.slice(0, 5).map((trend) => ({ + type: 'trend', + id: trend.clusterId, + title: `${trend.errorType}: ${trend.occurrenceCount}`, + relevanceScore: 0.8, + data: trend, + })), + suggestedActions: [ + 'View trend chart', + 'Compare with previous period', + 'Export trend data', + ], + executionTimeMs: Date.now() - startTime, + }; +} + +async function executeImpactQuery( + parsedQuery: ParsedQuery, + context: ExecutionContext, + startTime: number +): Promise { + const { entities } = parsedQuery; + const productId = context.productId || entities.products?.[0]; + + if (!productId) { + return createErrorResult(parsedQuery, startTime, 'No product specified'); + } + + // Get active clusters + const clusters = await repository.findClustersByProduct(productId, { + status: 'active', + limit: 20, + }); + + const totalAffectedUsers = clusters.reduce((sum, c) => sum + c.uniqueUsers, 0); + const totalOccurrences = clusters.reduce((sum, c) => sum + c.occurrenceCount, 0); + + // Get top error types + const topTypes = await repository.getTopErrorTypes(productId, 5); + + const aiResponse = `Error impact assessment for ${productId}: + +**Overall Impact:** +- Active error clusters: ${clusters.length} +- Total occurrences: ${totalOccurrences} +- Estimated affected users: ${totalAffectedUsers} + +**Top Error Types:** +${topTypes.map((t, i) => `${i + 1}. ${t.errorType}: ${t.count} clusters, ${t.totalOccurrences} total occurrences`).join('\n') || 'No data available'} + +**Recommendations:** +${clusters.length > 5 ? '- Focus on the top 5 most frequent errors for maximum impact' : '- Review all active error clusters'} +${totalAffectedUsers > 100 ? '- High user impact: prioritize investigation' : '- Moderate user impact: scheduled review recommended'}`; + + return { + query: parsedQuery.rawQuery, + intent: 'impact', + aiResponse, + confidence: 0.8, + supportingData: clusters.slice(0, 5).map((cluster) => ({ + type: 'cluster', + id: cluster.id, + title: `${cluster.errorType} (${cluster.uniqueUsers} users)`, + relevanceScore: 0.85, + data: cluster, + })), + suggestedActions: [ + 'View affected users', + 'Check severity breakdown', + 'Generate incident report', + ], + executionTimeMs: Date.now() - startTime, + }; +} + +async function executeGenericQuery( + parsedQuery: ParsedQuery, + context: ExecutionContext, + startTime: number +): Promise { + // Fallback for unhandled intents + return { + query: parsedQuery.rawQuery, + intent: parsedQuery.intent, + aiResponse: 'I understand you want information about errors, but I need more specific details. Try asking:\n\n- "Why did [error type] occur?"\n- "Show me similar [error type] errors"\n- "How many users were affected by [issue]?"\n- "Compare error trends over time"', + confidence: 0.3, + supportingData: [], + suggestedActions: [ + 'View all error clusters', + 'Search by error type', + 'Browse by platform', + ], + executionTimeMs: Date.now() - startTime, + }; +} + +// ============================================================================ +// Response Generation Helpers +// ============================================================================ + +function formatInsightResponse(insight: Partial): string { + const parts: string[] = []; + + parts.push(`**Root Cause Category:** ${insight.rootCauseCategory || 'Unknown'}`); + parts.push(`**Confidence:** ${insight.confidence || 'medium'} (${((insight.confidenceScore || 0) * 100).toFixed(0)}%)`); + parts.push(''); + parts.push(`**Hypothesis:** ${insight.hypothesis || 'No hypothesis generated'}`); + parts.push(''); + parts.push(`**Reasoning:** ${insight.reasoning || 'Analysis based on error patterns and telemetry data'}`); + + if (insight.evidence && insight.evidence.length > 0) { + parts.push(''); + parts.push('**Evidence:**'); + for (const ev of insight.evidence) { + parts.push(`- ${ev.type}: ${ev.description} (${ev.strength})`); + } + } + + if (insight.potentialFixDirection) { + parts.push(''); + parts.push(`**Suggested Fix:** ${insight.potentialFixDirection}`); + } + + return parts.join('\n'); +} + +function createErrorResult( + parsedQuery: ParsedQuery, + startTime: number, + errorMessage: string +): QueryExecutionResult { + return { + query: parsedQuery.rawQuery, + intent: parsedQuery.intent, + aiResponse: `Unable to process query: ${errorMessage}. Please check your query and try again.`, + confidence: 0, + supportingData: [], + suggestedActions: ['Check product ID', 'Verify time range', 'Simplify query'], + executionTimeMs: Date.now() - startTime, + }; +} + +function createEmptyResult( + parsedQuery: ParsedQuery, + startTime: number, + message: string +): QueryExecutionResult { + return { + query: parsedQuery.rawQuery, + intent: parsedQuery.intent, + aiResponse: message, + confidence: 0.3, + supportingData: [], + suggestedActions: ['Adjust search criteria', 'Expand time range', 'Try different keywords'], + executionTimeMs: Date.now() - startTime, + }; +} diff --git a/services/platform-service/src/modules/ai-diagnostics/query-parser.ts b/services/platform-service/src/modules/ai-diagnostics/query-parser.ts index 9782f922..3937d7d3 100644 --- a/services/platform-service/src/modules/ai-diagnostics/query-parser.ts +++ b/services/platform-service/src/modules/ai-diagnostics/query-parser.ts @@ -4,7 +4,7 @@ import type { QueryIntent, ExtractedEntities } from './types.js'; // Natural Language Query Parser // ============================================================================ -interface ParsedQuery { +export interface ParsedQuery { rawQuery: string; intent: QueryIntent; entities: ExtractedEntities;