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
|
// Natural Language Query Parser
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
interface ParsedQuery {
|
export interface ParsedQuery {
|
||||||
rawQuery: string;
|
rawQuery: string;
|
||||||
intent: QueryIntent;
|
intent: QueryIntent;
|
||||||
entities: ExtractedEntities;
|
entities: ExtractedEntities;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user