feat(ai-diagnostics): add query parser and executor [3.1-3.2]
This commit is contained in:
parent
44fa045ec5
commit
71cbb570ef
@ -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,
|
||||
};
|
||||
}
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user