feat(ai-diagnostics): add query parser and executor [3.1-3.2]

This commit is contained in:
saravanakumardb1 2026-03-03 11:53:59 -08:00
parent 44fa045ec5
commit 71cbb570ef
2 changed files with 503 additions and 1 deletions

View File

@ -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<QueryExecutionResult> {
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<QueryExecutionResult> {
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<QueryExecutionResult> {
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<QueryExecutionResult> {
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<QueryExecutionResult> {
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<QueryExecutionResult> {
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<QueryExecutionResult> {
// 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<DiagnosticInsightDoc>): 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,
};
}

View File

@ -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;