@duckcodeailabs/dql-agent 1.6.7 → 1.6.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/answer-loop.d.ts +12 -0
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +998 -52
- package/dist/answer-loop.js.map +1 -1
- package/dist/app-builder.d.ts +27 -0
- package/dist/app-builder.d.ts.map +1 -1
- package/dist/app-builder.js +191 -14
- package/dist/app-builder.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/metadata/analysis-planner.d.ts +52 -0
- package/dist/metadata/analysis-planner.d.ts.map +1 -0
- package/dist/metadata/analysis-planner.js +632 -0
- package/dist/metadata/analysis-planner.js.map +1 -0
- package/dist/metadata/catalog.d.ts +32 -0
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +705 -22
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/sql-context-validation.d.ts.map +1 -1
- package/dist/metadata/sql-context-validation.js +41 -13
- package/dist/metadata/sql-context-validation.js.map +1 -1
- package/dist/metadata/sql-shape.d.ts +13 -0
- package/dist/metadata/sql-shape.d.ts.map +1 -0
- package/dist/metadata/sql-shape.js +92 -0
- package/dist/metadata/sql-shape.js.map +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
const STOP_WORDS = new Set([
|
|
2
|
+
'about', 'after', 'again', 'against', 'all', 'also', 'and', 'answer', 'any', 'are', 'around',
|
|
3
|
+
'based', 'before', 'between', 'build', 'can', 'complete', 'could', 'data', 'dataset', 'datasets',
|
|
4
|
+
'deep', 'detail', 'detailed', 'does', 'for', 'from', 'give', 'have', 'help', 'into', 'latest',
|
|
5
|
+
'more', 'need', 'needs', 'over', 'please', 'provide', 'question', 'research', 'reserach', 'show',
|
|
6
|
+
'that', 'the', 'their', 'them', 'this', 'through', 'use', 'using', 'what', 'when', 'where',
|
|
7
|
+
'which', 'with', 'would', 'you',
|
|
8
|
+
]);
|
|
9
|
+
const METRIC_WORDS = [
|
|
10
|
+
'amount', 'arr', 'average', 'avg', 'balance', 'bookings', 'churn', 'conversion', 'cost',
|
|
11
|
+
'count', 'duration', 'expense', 'growth', 'kpi', 'margin', 'metric', 'mrr', 'orders',
|
|
12
|
+
'points', 'profit', 'quantity', 'rate', 'revenue', 'sales', 'score', 'scorer', 'scoring', 'spend', 'stats',
|
|
13
|
+
'statistics', 'total', 'usage', 'value', 'volume',
|
|
14
|
+
];
|
|
15
|
+
const DIMENSION_WORDS = [
|
|
16
|
+
'account', 'category', 'channel', 'cohort', 'customer', 'department', 'entity', 'geo',
|
|
17
|
+
'location', 'market', 'merchant', 'month', 'person', 'player', 'product', 'profile',
|
|
18
|
+
'region', 'segment', 'sku', 'team', 'type', 'user', 'vendor', 'week', 'year',
|
|
19
|
+
];
|
|
20
|
+
const PROFILE_WORDS = [
|
|
21
|
+
'bio', 'biography', 'career', 'complete stats', 'details', 'entity 360', 'overview',
|
|
22
|
+
'profile', 'record', 'snapshot', 'stats', 'statistics', 'summary',
|
|
23
|
+
];
|
|
24
|
+
export function buildAnalysisQuestionPlan(question, followUp) {
|
|
25
|
+
const cleanQuestion = question.replace(/\s+/g, ' ').trim();
|
|
26
|
+
const normalizedQuestion = normalizeSearchText(cleanQuestion);
|
|
27
|
+
const lower = cleanQuestion.toLowerCase();
|
|
28
|
+
const entities = extractEntities(cleanQuestion);
|
|
29
|
+
const metricTerms = extractMetricTerms(cleanQuestion);
|
|
30
|
+
const dimensionTerms = extractDimensionTerms(cleanQuestion);
|
|
31
|
+
const filterTerms = extractFilterTerms(cleanQuestion, entities);
|
|
32
|
+
const timeTerms = extractTimeTerms(cleanQuestion);
|
|
33
|
+
const mode = inferQuestionMode({ question: cleanQuestion, lower, entities, metricTerms, dimensionTerms, followUp });
|
|
34
|
+
const routeIntent = routeIntentForMode(mode);
|
|
35
|
+
const outputShape = outputShapeForMode(mode, lower);
|
|
36
|
+
const shouldConsiderCertifiedExact = certifiedExactIsPlausible(mode, entities);
|
|
37
|
+
const needsGeneratedSql = generatedSqlIsLikely(mode, shouldConsiderCertifiedExact);
|
|
38
|
+
const needsResearchWorkspace = researchWorkspaceIsLikely(mode, lower);
|
|
39
|
+
const searchTerms = uniqueStrings([
|
|
40
|
+
...tokenize(cleanQuestion),
|
|
41
|
+
...entities.flatMap((entity) => tokenize(entity.text)),
|
|
42
|
+
...metricTerms,
|
|
43
|
+
...dimensionTerms,
|
|
44
|
+
...filterTerms,
|
|
45
|
+
...timeTerms,
|
|
46
|
+
...modeSearchTerms(mode),
|
|
47
|
+
]).filter((term) => !STOP_WORDS.has(term)).slice(0, 36);
|
|
48
|
+
const searchQueries = buildSearchQueries({
|
|
49
|
+
question: cleanQuestion,
|
|
50
|
+
mode,
|
|
51
|
+
entities,
|
|
52
|
+
metricTerms,
|
|
53
|
+
dimensionTerms,
|
|
54
|
+
filterTerms,
|
|
55
|
+
timeTerms,
|
|
56
|
+
searchTerms,
|
|
57
|
+
});
|
|
58
|
+
const reasons = buildPlanReasons(mode, entities, metricTerms, dimensionTerms, timeTerms, shouldConsiderCertifiedExact);
|
|
59
|
+
return {
|
|
60
|
+
question: cleanQuestion,
|
|
61
|
+
normalizedQuestion,
|
|
62
|
+
mode,
|
|
63
|
+
routeIntent,
|
|
64
|
+
entities,
|
|
65
|
+
metricTerms,
|
|
66
|
+
dimensionTerms,
|
|
67
|
+
filterTerms,
|
|
68
|
+
timeTerms,
|
|
69
|
+
outputShape,
|
|
70
|
+
needsGeneratedSql,
|
|
71
|
+
shouldConsiderCertifiedExact,
|
|
72
|
+
needsResearchWorkspace,
|
|
73
|
+
searchQueries,
|
|
74
|
+
searchTerms,
|
|
75
|
+
confidence: planConfidence(mode, entities, metricTerms, dimensionTerms),
|
|
76
|
+
reasons,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function certifiedApplicabilityForObject(object, plan) {
|
|
80
|
+
const reasons = [];
|
|
81
|
+
if (object.objectType !== 'dql_block' || !isCertifiedObject(object)) {
|
|
82
|
+
return { objectKey: object.objectKey, name: object.name, kind: 'not_applicable', score: 0, reasons: ['not a certified DQL block'] };
|
|
83
|
+
}
|
|
84
|
+
const text = objectSearchText(object);
|
|
85
|
+
const questionText = plan.normalizedQuestion;
|
|
86
|
+
const name = normalizeSearchText(object.name);
|
|
87
|
+
const explicitName = Boolean(name && questionText.includes(name));
|
|
88
|
+
const exactExample = hasExactExampleQuestion(object, plan.normalizedQuestion);
|
|
89
|
+
const metricMatch = scoreTerms(text, plan.metricTerms);
|
|
90
|
+
const dimensionMatch = scoreTerms(text, plan.dimensionTerms);
|
|
91
|
+
const searchMatch = scoreTerms(text, plan.searchTerms);
|
|
92
|
+
const directionCompatible = rankingDirectionCompatible(plan.question, directionalTextForObject(object));
|
|
93
|
+
const asksDifferentGrain = plan.needsGeneratedSql || plan.entities.length > 0 || plan.mode === 'entity_profile';
|
|
94
|
+
const score = Number(((explicitName ? 45 : 0) +
|
|
95
|
+
(exactExample ? 55 : 0) +
|
|
96
|
+
metricMatch.score * 12 +
|
|
97
|
+
dimensionMatch.score * 8 +
|
|
98
|
+
searchMatch.score * 3 +
|
|
99
|
+
(directionCompatible ? 10 : -25) +
|
|
100
|
+
(isDirectCertifiedQuestion(plan) ? 15 : 0) -
|
|
101
|
+
(asksDifferentGrain ? 24 : 0)).toFixed(3));
|
|
102
|
+
if (explicitName)
|
|
103
|
+
reasons.push('question names this certified block');
|
|
104
|
+
if (exactExample)
|
|
105
|
+
reasons.push('question matches a certified example');
|
|
106
|
+
if (metricMatch.matched.length)
|
|
107
|
+
reasons.push(`metric terms matched: ${metricMatch.matched.join(', ')}`);
|
|
108
|
+
if (dimensionMatch.matched.length)
|
|
109
|
+
reasons.push(`dimension terms matched: ${dimensionMatch.matched.join(', ')}`);
|
|
110
|
+
if (!directionCompatible)
|
|
111
|
+
reasons.push('ranking direction conflicts with certified block wording');
|
|
112
|
+
const hasRankingEvidence = plan.mode !== 'ranking' || Boolean(rankingDirection(text)) || /\b(order\s+by|rank|ranking|limit|top|bottom|leader|leading)\b/i.test(text);
|
|
113
|
+
const hasMetricEvidence = metricMatch.score > 0 || exactExample || explicitName || plan.metricTerms.length === 0;
|
|
114
|
+
if ((explicitName || exactExample || score >= 70 || (plan.shouldConsiderCertifiedExact &&
|
|
115
|
+
score >= 40 &&
|
|
116
|
+
hasMetricEvidence &&
|
|
117
|
+
(metricMatch.score > 0 || dimensionMatch.score > 0 || searchMatch.score >= 2))) && directionCompatible && hasRankingEvidence && !asksDifferentGrain) {
|
|
118
|
+
return {
|
|
119
|
+
objectKey: object.objectKey,
|
|
120
|
+
name: object.name,
|
|
121
|
+
kind: 'exact_answer',
|
|
122
|
+
score,
|
|
123
|
+
reasons: reasons.length ? reasons : ['certified block contract matches the requested answer shape'],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const parameterPolicy = metadataString(object.payload?.parameterPolicy ?? object.payload?.parameter_policy);
|
|
127
|
+
if (parameterPolicy && /safe_filter|safe_group_by|template/i.test(parameterPolicy) && score >= 58 && directionCompatible) {
|
|
128
|
+
return {
|
|
129
|
+
objectKey: object.objectKey,
|
|
130
|
+
name: object.name,
|
|
131
|
+
kind: 'safe_parameterized',
|
|
132
|
+
score,
|
|
133
|
+
reasons: [...reasons, `certified block declares parameter policy ${parameterPolicy}`],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (score >= 28 || metricMatch.score > 0 || dimensionMatch.score > 0) {
|
|
137
|
+
return {
|
|
138
|
+
objectKey: object.objectKey,
|
|
139
|
+
name: object.name,
|
|
140
|
+
kind: 'context_only',
|
|
141
|
+
score,
|
|
142
|
+
reasons: [
|
|
143
|
+
...reasons,
|
|
144
|
+
asksDifferentGrain
|
|
145
|
+
? 'question asks for a different entity, filter, or grain; use certified block as context only'
|
|
146
|
+
: 'certified block is relevant but not exact enough for direct execution',
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
objectKey: object.objectKey,
|
|
152
|
+
name: object.name,
|
|
153
|
+
kind: 'not_applicable',
|
|
154
|
+
score,
|
|
155
|
+
reasons: reasons.length ? reasons : ['low semantic overlap with the requested analysis'],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
export function scoreMetadataObjectWithAnalysisPlan(object, plan) {
|
|
159
|
+
const text = objectSearchText(object);
|
|
160
|
+
const search = scoreTerms(text, plan.searchTerms);
|
|
161
|
+
const metrics = scoreTerms(text, plan.metricTerms);
|
|
162
|
+
const dimensions = scoreTerms(text, plan.dimensionTerms);
|
|
163
|
+
const entity = scoreTerms(text, plan.entities.flatMap((item) => tokenize(item.text)));
|
|
164
|
+
const sourceShape = scoreSourceShape(object, plan);
|
|
165
|
+
const certified = certifiedApplicabilityForObject(object, plan);
|
|
166
|
+
const score = Number((search.score * 2.6 +
|
|
167
|
+
metrics.score * 7 +
|
|
168
|
+
dimensions.score * 5 +
|
|
169
|
+
entity.score * 4 +
|
|
170
|
+
sourceShape +
|
|
171
|
+
(certified.kind === 'exact_answer' ? 36 : certified.kind === 'safe_parameterized' ? 28 : certified.kind === 'context_only' ? 14 : 0)).toFixed(3));
|
|
172
|
+
const reasons = [
|
|
173
|
+
search.matched.length ? `analysis terms matched: ${search.matched.slice(0, 5).join(', ')}` : '',
|
|
174
|
+
metrics.matched.length ? `metric terms matched: ${metrics.matched.slice(0, 4).join(', ')}` : '',
|
|
175
|
+
dimensions.matched.length ? `dimension terms matched: ${dimensions.matched.slice(0, 4).join(', ')}` : '',
|
|
176
|
+
entity.matched.length ? `entity terms matched: ${entity.matched.slice(0, 4).join(', ')}` : '',
|
|
177
|
+
sourceShape > 0 ? 'source shape matches requested analytical task' : '',
|
|
178
|
+
certified.kind !== 'not_applicable' ? `certified applicability: ${certified.kind}` : '',
|
|
179
|
+
].filter(Boolean);
|
|
180
|
+
return { score, reasons };
|
|
181
|
+
}
|
|
182
|
+
export function sortAllowedSqlContextForAnalysisPlan(context, plan, options = {}) {
|
|
183
|
+
const relations = [...context.relations]
|
|
184
|
+
.sort((a, b) => scoreAllowedSqlRelationWithAnalysisPlan(b, plan).score - scoreAllowedSqlRelationWithAnalysisPlan(a, plan).score
|
|
185
|
+
|| a.relation.localeCompare(b.relation))
|
|
186
|
+
.slice(0, options.maxRelations ?? 40);
|
|
187
|
+
return {
|
|
188
|
+
relations,
|
|
189
|
+
sourceBlockSql: context.sourceBlockSql,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
export function scoreAllowedSqlRelationWithAnalysisPlan(relation, plan) {
|
|
193
|
+
const text = normalizeSearchText(relationSearchText(relation));
|
|
194
|
+
const search = scoreTerms(text, plan.searchTerms);
|
|
195
|
+
const metrics = scoreTerms(text, plan.metricTerms);
|
|
196
|
+
const dimensions = scoreTerms(text, plan.dimensionTerms);
|
|
197
|
+
const entities = scoreTerms(text, plan.entities.flatMap((entity) => tokenize(entity.text)));
|
|
198
|
+
const shape = scoreSourceShape({
|
|
199
|
+
objectKey: relation.objectKey ?? relation.relation,
|
|
200
|
+
objectType: 'runtime_table',
|
|
201
|
+
name: relation.name,
|
|
202
|
+
fullName: relation.relation,
|
|
203
|
+
sourceSystem: relation.source,
|
|
204
|
+
payload: { columns: relation.columns },
|
|
205
|
+
}, plan);
|
|
206
|
+
const sourceBonus = relation.source.includes('certified source SQL') ? 14
|
|
207
|
+
: relation.source.includes('certified block') ? 8
|
|
208
|
+
: relation.source.includes('runtime') ? 6
|
|
209
|
+
: 0;
|
|
210
|
+
const usabilityBonus = generatedSqlUsabilityScore(relation, plan);
|
|
211
|
+
const columnShapeBonus = relationColumnShapeScore(relation, plan);
|
|
212
|
+
const score = Number((search.score * 3 +
|
|
213
|
+
metrics.score * 8 +
|
|
214
|
+
dimensions.score * 6 +
|
|
215
|
+
entities.score * 3 +
|
|
216
|
+
shape +
|
|
217
|
+
sourceBonus +
|
|
218
|
+
usabilityBonus +
|
|
219
|
+
columnShapeBonus +
|
|
220
|
+
Math.min(relation.columns.length, 24) * 0.2).toFixed(3));
|
|
221
|
+
const reasons = [
|
|
222
|
+
search.matched.length ? `analysis terms matched: ${search.matched.slice(0, 5).join(', ')}` : '',
|
|
223
|
+
metrics.matched.length ? `metric terms matched: ${metrics.matched.slice(0, 4).join(', ')}` : '',
|
|
224
|
+
dimensions.matched.length ? `dimension terms matched: ${dimensions.matched.slice(0, 4).join(', ')}` : '',
|
|
225
|
+
entities.matched.length ? `entity terms matched: ${entities.matched.slice(0, 4).join(', ')}` : '',
|
|
226
|
+
shape > 0 ? 'relation shape matches requested analysis mode' : '',
|
|
227
|
+
sourceBonus > 0 ? `trusted source context: ${relation.source}` : '',
|
|
228
|
+
usabilityBonus > 0 ? 'relation has usable columns for generated SQL' : '',
|
|
229
|
+
usabilityBonus < 0 ? 'relation has no inspected/projected columns for generated SQL' : '',
|
|
230
|
+
columnShapeBonus > 0 ? 'columns match requested analytical shape' : '',
|
|
231
|
+
relation.columns.length > 0 ? `${relation.columns.length} inspected/projected columns` : '',
|
|
232
|
+
].filter(Boolean);
|
|
233
|
+
return {
|
|
234
|
+
relation: relation.relation,
|
|
235
|
+
name: relation.name,
|
|
236
|
+
source: relation.source,
|
|
237
|
+
score,
|
|
238
|
+
reasons,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function generatedSqlUsabilityScore(relation, plan) {
|
|
242
|
+
if (!plan.needsGeneratedSql)
|
|
243
|
+
return 0;
|
|
244
|
+
if (relation.columns.length === 0)
|
|
245
|
+
return -14;
|
|
246
|
+
return Math.min(18, 8 + relation.columns.length * 0.8);
|
|
247
|
+
}
|
|
248
|
+
function relationColumnShapeScore(relation, plan) {
|
|
249
|
+
const columns = relation.columns.map((column) => normalizeSearchText(column.name));
|
|
250
|
+
if (columns.length === 0)
|
|
251
|
+
return 0;
|
|
252
|
+
let score = 0;
|
|
253
|
+
if (plan.mode === 'entity_profile') {
|
|
254
|
+
if (columns.some((column) => /\b(player|customer|account|user|member|person|product|vendor|name|title)\b/.test(column))) {
|
|
255
|
+
score += 7;
|
|
256
|
+
}
|
|
257
|
+
if (columns.some((column) => /\b(total|count|score|point|assist|rebound|game|minute|revenue|amount|spend|order|stat|rate|value)\b/.test(column))) {
|
|
258
|
+
score += 9;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (plan.mode === 'ranking' && columns.some((column) => /\b(total|count|score|point|amount|revenue|sales|order|rank|value)\b/.test(column))) {
|
|
262
|
+
score += 8;
|
|
263
|
+
}
|
|
264
|
+
if ((plan.mode === 'trend' || plan.mode === 'diagnose_change') && columns.some((column) => /\b(date|time|day|week|month|quarter|year|season|period)\b/.test(column))) {
|
|
265
|
+
score += 8;
|
|
266
|
+
}
|
|
267
|
+
return score;
|
|
268
|
+
}
|
|
269
|
+
function inferQuestionMode(input) {
|
|
270
|
+
const { lower } = input;
|
|
271
|
+
if (followUpKind(input.followUp) === 'drilldown')
|
|
272
|
+
return 'entity_drilldown';
|
|
273
|
+
if (/^\s*(run|execute|open)\b/i.test(lower))
|
|
274
|
+
return 'exact_lookup';
|
|
275
|
+
if (/\b(trust|rely|certif|certified|lineage|owner|caveat|gap|governance|can .* trust)\b/i.test(lower))
|
|
276
|
+
return 'trust_review';
|
|
277
|
+
if (/\b(define|definition|meaning of|what is|what are|what does .+ mean)\b/i.test(lower))
|
|
278
|
+
return 'definition';
|
|
279
|
+
if (/\b(anomal|exception|outlier|spike|dip)\b/i.test(lower))
|
|
280
|
+
return 'anomaly';
|
|
281
|
+
if (/\b(compare|versus|vs\.?|cohort)\b/i.test(lower))
|
|
282
|
+
return 'comparison';
|
|
283
|
+
if (/\b(why|changed?|change|drop|dropped|decline|declined|increase|increased|decrease|decreased|delta|variance|what happened)\b/i.test(lower))
|
|
284
|
+
return 'diagnose_change';
|
|
285
|
+
if (/\b(driver|drivers|drove|break\s*down|breakdown|contribute|contribution|top movers?)\b/i.test(lower))
|
|
286
|
+
return 'driver_breakdown';
|
|
287
|
+
if (/\b(profile|overview|360|complete\s+(?:stats|statistics|view)|full\s+(?:stats|statistics|view)|all\s+(?:stats|statistics|metrics)|research|reserach)\b/i.test(lower))
|
|
288
|
+
return 'entity_profile';
|
|
289
|
+
if (input.entities.length > 0 && (input.metricTerms.length > 0 || /\b(show|list|find|give|provide|performance|activity|history|details)\b/i.test(lower)))
|
|
290
|
+
return 'entity_drilldown';
|
|
291
|
+
if (/\b(trend|over time|by\s+(?:day|week|month|quarter|year|season)|daily|weekly|monthly|quarterly|yearly)\b/i.test(lower))
|
|
292
|
+
return 'trend';
|
|
293
|
+
if (/\b(top|bottom|best|worst|highest|lowest|least|fewest|minimum|min|maximum|max|rank|ranking|most|leading|leader|leaders)\b/i.test(lower))
|
|
294
|
+
return 'ranking';
|
|
295
|
+
if (/\b(block|certified|saved|existing|approved|governed)\b/i.test(lower))
|
|
296
|
+
return 'exact_lookup';
|
|
297
|
+
if (isDirectKpiValueQuestion(lower))
|
|
298
|
+
return 'exact_lookup';
|
|
299
|
+
if (/\b(show|list|find|which|who|how many|how much|metric|kpi|dashboard|performance|revenue|sales|orders|customers|users)\b/i.test(lower))
|
|
300
|
+
return 'general_analysis';
|
|
301
|
+
return 'clarify';
|
|
302
|
+
}
|
|
303
|
+
function routeIntentForMode(mode) {
|
|
304
|
+
switch (mode) {
|
|
305
|
+
case 'exact_lookup': return 'exact_certified_lookup';
|
|
306
|
+
case 'definition': return 'definition_lookup';
|
|
307
|
+
case 'ranking': return 'ad_hoc_ranking';
|
|
308
|
+
case 'entity_profile':
|
|
309
|
+
case 'entity_drilldown': return 'entity_drilldown';
|
|
310
|
+
case 'trend': return 'segment_compare';
|
|
311
|
+
case 'comparison': return 'segment_compare';
|
|
312
|
+
case 'driver_breakdown': return 'driver_breakdown';
|
|
313
|
+
case 'diagnose_change': return 'diagnose_change';
|
|
314
|
+
case 'anomaly': return 'anomaly_investigation';
|
|
315
|
+
case 'trust_review': return 'trust_gap_review';
|
|
316
|
+
case 'general_analysis': return 'ad_hoc_ranking';
|
|
317
|
+
case 'clarify': return 'clarify';
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function certifiedExactIsPlausible(mode, entities) {
|
|
321
|
+
if (mode === 'exact_lookup' || mode === 'definition')
|
|
322
|
+
return true;
|
|
323
|
+
if (mode === 'ranking' || mode === 'general_analysis')
|
|
324
|
+
return entities.length === 0;
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
function generatedSqlIsLikely(mode, certifiedExact) {
|
|
328
|
+
if (mode === 'clarify' || mode === 'definition' || mode === 'trust_review')
|
|
329
|
+
return false;
|
|
330
|
+
return !certifiedExact || ['entity_profile', 'entity_drilldown', 'comparison', 'driver_breakdown', 'diagnose_change', 'anomaly', 'trend'].includes(mode);
|
|
331
|
+
}
|
|
332
|
+
function researchWorkspaceIsLikely(mode, lower) {
|
|
333
|
+
return ['entity_profile', 'driver_breakdown', 'diagnose_change', 'anomaly', 'trust_review'].includes(mode)
|
|
334
|
+
|| /\b(deep\s+research|research|reserach|investigate|investigation|root cause)\b/i.test(lower);
|
|
335
|
+
}
|
|
336
|
+
function outputShapeForMode(mode, lower) {
|
|
337
|
+
if (mode === 'entity_profile')
|
|
338
|
+
return 'profile';
|
|
339
|
+
if (mode === 'definition' || mode === 'trust_review')
|
|
340
|
+
return 'narrative';
|
|
341
|
+
if (/\b(chart|graph|visual|plot)\b/i.test(lower) || mode === 'trend')
|
|
342
|
+
return 'chart';
|
|
343
|
+
if (/\b(table|list|show all|complete|full)\b/i.test(lower) || mode === 'ranking' || mode === 'entity_drilldown')
|
|
344
|
+
return 'table';
|
|
345
|
+
if (/\bwhat is|how many|how much|total|count|kpi|metric\b/i.test(lower))
|
|
346
|
+
return 'value';
|
|
347
|
+
return 'narrative';
|
|
348
|
+
}
|
|
349
|
+
function extractEntities(question) {
|
|
350
|
+
const entities = [];
|
|
351
|
+
for (const match of question.matchAll(/["']([^"']{2,120})["']/g)) {
|
|
352
|
+
entities.push({ text: match[1].trim(), source: 'quoted' });
|
|
353
|
+
}
|
|
354
|
+
for (const match of question.matchAll(/\b[\w.%+-]+@[\w.-]+\.[A-Za-z]{2,}\b/g)) {
|
|
355
|
+
entities.push({ text: match[0], source: 'email', typeHint: 'email' });
|
|
356
|
+
}
|
|
357
|
+
for (const match of question.matchAll(/\b(?:id|account id|customer id|user id|sku)\s*[:=]?\s*([A-Za-z0-9_.-]{3,80})\b/gi)) {
|
|
358
|
+
entities.push({ text: match[1].trim(), source: 'id' });
|
|
359
|
+
}
|
|
360
|
+
for (const match of question.matchAll(/\b(?:for|where|only|specific|named|called|profile\s+for|research\s+on|reserach\s+on)\s+([A-Z][A-Za-z0-9&.'-]+(?:\s+[A-Z][A-Za-z0-9&.'-]+){0,5})/g)) {
|
|
361
|
+
entities.push({ text: cleanEntityText(match[1]), source: 'explicit_filter' });
|
|
362
|
+
}
|
|
363
|
+
for (const match of question.matchAll(/\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+){1,4})\b/g)) {
|
|
364
|
+
const text = cleanEntityText(match[1]);
|
|
365
|
+
if (text && !/^(SQL|DQL|KPI|ARR|MRR|NBA|AI|LLM)$/.test(text)) {
|
|
366
|
+
entities.push({ text, source: 'named_entity' });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return uniqueEntities(entities).slice(0, 8);
|
|
370
|
+
}
|
|
371
|
+
function extractMetricTerms(question) {
|
|
372
|
+
const lower = question.toLowerCase();
|
|
373
|
+
const terms = new Set();
|
|
374
|
+
if (/\b(scorer|scorers|scoring|scored)\b/i.test(lower)) {
|
|
375
|
+
terms.add('score');
|
|
376
|
+
terms.add('scoring');
|
|
377
|
+
}
|
|
378
|
+
for (const word of METRIC_WORDS) {
|
|
379
|
+
if (new RegExp(`\\b${escapeRegExp(word)}s?\\b`, 'i').test(lower))
|
|
380
|
+
terms.add(normalizeTerm(word));
|
|
381
|
+
}
|
|
382
|
+
for (const match of lower.matchAll(/\b(total|average|avg|count|sum|min|max)\s+([a-z][a-z0-9_ -]{2,40})/g)) {
|
|
383
|
+
terms.add(normalizeTerm(`${match[1]} ${match[2]}`));
|
|
384
|
+
terms.add(normalizeTerm(match[2]));
|
|
385
|
+
}
|
|
386
|
+
return uniqueStrings([...terms]).slice(0, 16);
|
|
387
|
+
}
|
|
388
|
+
function extractDimensionTerms(question) {
|
|
389
|
+
const lower = question.toLowerCase();
|
|
390
|
+
const terms = new Set();
|
|
391
|
+
for (const word of DIMENSION_WORDS) {
|
|
392
|
+
if (new RegExp(`\\b${escapeRegExp(word)}s?\\b`, 'i').test(lower))
|
|
393
|
+
terms.add(normalizeTerm(word));
|
|
394
|
+
}
|
|
395
|
+
for (const match of lower.matchAll(/\bby\s+([a-z][a-z0-9_ -]{1,40})/g)) {
|
|
396
|
+
const value = match[1].replace(/\b(where|for|with|and|or|from|over|in)\b.*$/i, '').trim();
|
|
397
|
+
if (value)
|
|
398
|
+
terms.add(normalizeTerm(value));
|
|
399
|
+
}
|
|
400
|
+
return uniqueStrings([...terms]).slice(0, 16);
|
|
401
|
+
}
|
|
402
|
+
function extractFilterTerms(question, entities) {
|
|
403
|
+
return uniqueStrings([
|
|
404
|
+
...entities.flatMap((entity) => tokenize(entity.text)),
|
|
405
|
+
...Array.from(question.matchAll(/\b(?:last|this|next|previous|prior|current)\s+(day|week|month|quarter|year|season)\b/gi)).map((match) => match[0].toLowerCase()),
|
|
406
|
+
]).slice(0, 16);
|
|
407
|
+
}
|
|
408
|
+
function extractTimeTerms(question) {
|
|
409
|
+
const terms = [];
|
|
410
|
+
for (const match of question.matchAll(/\b(?:today|yesterday|ytd|mtd|qtd|wtd|last\s+\w+|this\s+\w+|next\s+\w+|previous\s+\w+|prior\s+\w+|\d{4})\b/gi)) {
|
|
411
|
+
terms.push(match[0].toLowerCase());
|
|
412
|
+
}
|
|
413
|
+
for (const word of ['date', 'day', 'week', 'month', 'quarter', 'year', 'season', 'period']) {
|
|
414
|
+
if (new RegExp(`\\b${word}\\b`, 'i').test(question))
|
|
415
|
+
terms.push(word);
|
|
416
|
+
}
|
|
417
|
+
return uniqueStrings(terms).slice(0, 12);
|
|
418
|
+
}
|
|
419
|
+
function buildSearchQueries(input) {
|
|
420
|
+
const entityText = input.entities.map((entity) => entity.text).join(' ');
|
|
421
|
+
const profileTerms = input.mode === 'entity_profile' ? PROFILE_WORDS.join(' ') : '';
|
|
422
|
+
return uniqueStrings([
|
|
423
|
+
input.question,
|
|
424
|
+
[entityText, ...input.metricTerms, ...input.dimensionTerms].filter(Boolean).join(' '),
|
|
425
|
+
[...input.metricTerms, ...input.dimensionTerms, ...input.timeTerms].join(' '),
|
|
426
|
+
[profileTerms, ...input.searchTerms.slice(0, 12)].filter(Boolean).join(' '),
|
|
427
|
+
input.searchTerms.slice(0, 18).join(' '),
|
|
428
|
+
].map((query) => query.replace(/\s+/g, ' ').trim()).filter(Boolean)).slice(0, 5);
|
|
429
|
+
}
|
|
430
|
+
function buildPlanReasons(mode, entities, metrics, dimensions, timeTerms, certifiedExact) {
|
|
431
|
+
return [
|
|
432
|
+
`classified as ${mode}`,
|
|
433
|
+
entities.length ? `entities: ${entities.map((entity) => entity.text).join(', ')}` : '',
|
|
434
|
+
metrics.length ? `metrics: ${metrics.join(', ')}` : '',
|
|
435
|
+
dimensions.length ? `dimensions: ${dimensions.join(', ')}` : '',
|
|
436
|
+
timeTerms.length ? `time context: ${timeTerms.join(', ')}` : '',
|
|
437
|
+
certifiedExact ? 'certified exact answer is plausible' : 'generated SQL may be needed if no exact certified contract matches',
|
|
438
|
+
].filter(Boolean);
|
|
439
|
+
}
|
|
440
|
+
function modeSearchTerms(mode) {
|
|
441
|
+
switch (mode) {
|
|
442
|
+
case 'entity_profile': return ['profile', 'summary', 'details', 'stats', 'statistics', 'history'];
|
|
443
|
+
case 'entity_drilldown': return ['detail', 'filter', 'name', 'id'];
|
|
444
|
+
case 'ranking': return ['rank', 'top', 'bottom', 'total'];
|
|
445
|
+
case 'trend': return ['date', 'time', 'month', 'week', 'year', 'trend'];
|
|
446
|
+
case 'comparison': return ['compare', 'segment', 'cohort'];
|
|
447
|
+
case 'driver_breakdown': return ['driver', 'breakdown', 'contribution'];
|
|
448
|
+
case 'diagnose_change': return ['change', 'baseline', 'period', 'date'];
|
|
449
|
+
case 'anomaly': return ['anomaly', 'exception', 'outlier'];
|
|
450
|
+
case 'trust_review': return ['lineage', 'owner', 'certified', 'caveat'];
|
|
451
|
+
default: return [];
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function scoreSourceShape(object, plan) {
|
|
455
|
+
const text = objectSearchText(object);
|
|
456
|
+
let score = 0;
|
|
457
|
+
if ((object.objectType === 'dbt_model' || object.objectType === 'warehouse_table' || object.objectType === 'runtime_table') && plan.needsGeneratedSql)
|
|
458
|
+
score += 12;
|
|
459
|
+
if (plan.mode === 'entity_profile' && /\b(dim|entity|profile|customer|account|user|product|player|vendor|person|member)\b/i.test(text))
|
|
460
|
+
score += 8;
|
|
461
|
+
if (plan.mode === 'entity_profile' && /\b(fct|fact|event|transaction|activity|performance|stats|history)\b/i.test(text))
|
|
462
|
+
score += 8;
|
|
463
|
+
if (plan.mode === 'trend' && /\b(date|time|day|week|month|quarter|year|season|period)\b/i.test(text))
|
|
464
|
+
score += 10;
|
|
465
|
+
if (plan.mode === 'ranking' && /\b(total|amount|score|count|rank|revenue|sales|points|orders)\b/i.test(text))
|
|
466
|
+
score += 8;
|
|
467
|
+
return score;
|
|
468
|
+
}
|
|
469
|
+
function relationSearchText(relation) {
|
|
470
|
+
return [
|
|
471
|
+
relation.relation,
|
|
472
|
+
relation.name,
|
|
473
|
+
relation.source,
|
|
474
|
+
relation.columns.map((column) => `${column.name} ${column.description ?? ''}`).join(' '),
|
|
475
|
+
].join(' ');
|
|
476
|
+
}
|
|
477
|
+
function isDirectCertifiedQuestion(plan) {
|
|
478
|
+
return plan.mode === 'exact_lookup' || plan.mode === 'definition' || (plan.shouldConsiderCertifiedExact && !plan.needsGeneratedSql);
|
|
479
|
+
}
|
|
480
|
+
function hasExactExampleQuestion(object, normalizedQuestion) {
|
|
481
|
+
const examples = Array.isArray(object.payload?.examples) ? object.payload.examples : [];
|
|
482
|
+
return examples.some((example) => {
|
|
483
|
+
if (!example || typeof example !== 'object')
|
|
484
|
+
return false;
|
|
485
|
+
const question = example.question;
|
|
486
|
+
return typeof question === 'string' && normalizeSearchText(question) === normalizedQuestion;
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
function rankingDirectionCompatible(question, targetText) {
|
|
490
|
+
const questionDirection = rankingDirection(question);
|
|
491
|
+
if (!questionDirection)
|
|
492
|
+
return true;
|
|
493
|
+
const targetDirection = rankingDirection(targetText);
|
|
494
|
+
return !targetDirection || targetDirection === questionDirection;
|
|
495
|
+
}
|
|
496
|
+
function rankingDirection(value) {
|
|
497
|
+
const lower = value.toLowerCase();
|
|
498
|
+
const bottom = /\b(bottom|worst|lowest|least|fewest|minimum|min|smallest)\b/.test(lower);
|
|
499
|
+
const top = /\b(top|best|highest|most|maximum|max|largest|leading|leaders?)\b/.test(lower);
|
|
500
|
+
if (bottom && !top)
|
|
501
|
+
return 'bottom';
|
|
502
|
+
if (top && !bottom)
|
|
503
|
+
return 'top';
|
|
504
|
+
return undefined;
|
|
505
|
+
}
|
|
506
|
+
function scoreTerms(text, terms) {
|
|
507
|
+
const matched = [];
|
|
508
|
+
let score = 0;
|
|
509
|
+
for (const term of uniqueStrings(terms.map(normalizeTerm).filter(Boolean))) {
|
|
510
|
+
if (!term || term.length < 2)
|
|
511
|
+
continue;
|
|
512
|
+
const tokens = term.split(/\s+/).filter(Boolean);
|
|
513
|
+
const hit = tokens.length > 1
|
|
514
|
+
? text.includes(term)
|
|
515
|
+
: new RegExp(`\\b${escapeRegExp(term)}s?\\b`, 'i').test(text);
|
|
516
|
+
if (!hit)
|
|
517
|
+
continue;
|
|
518
|
+
matched.push(term);
|
|
519
|
+
score += tokens.length > 1 ? 1.4 : 1;
|
|
520
|
+
}
|
|
521
|
+
return { score, matched };
|
|
522
|
+
}
|
|
523
|
+
function objectSearchText(object) {
|
|
524
|
+
return normalizeSearchText([
|
|
525
|
+
object.objectType,
|
|
526
|
+
object.objectKey,
|
|
527
|
+
object.name,
|
|
528
|
+
object.fullName ?? '',
|
|
529
|
+
object.domain ?? '',
|
|
530
|
+
object.owner ?? '',
|
|
531
|
+
object.status ?? '',
|
|
532
|
+
object.description ?? '',
|
|
533
|
+
object.sourceSystem ?? '',
|
|
534
|
+
JSON.stringify(object.payload ?? {}),
|
|
535
|
+
].join(' '));
|
|
536
|
+
}
|
|
537
|
+
function directionalTextForObject(object) {
|
|
538
|
+
return normalizeSearchText([
|
|
539
|
+
object.name,
|
|
540
|
+
object.description ?? '',
|
|
541
|
+
Array.isArray(object.payload?.tags) ? object.payload.tags.join(' ') : '',
|
|
542
|
+
typeof object.payload?.sql === 'string' ? object.payload.sql : '',
|
|
543
|
+
].join(' '));
|
|
544
|
+
}
|
|
545
|
+
function planConfidence(mode, entities, metrics, dimensions) {
|
|
546
|
+
let score = mode === 'clarify' ? 0.25 : 0.55;
|
|
547
|
+
if (entities.length)
|
|
548
|
+
score += 0.14;
|
|
549
|
+
if (metrics.length)
|
|
550
|
+
score += 0.12;
|
|
551
|
+
if (dimensions.length)
|
|
552
|
+
score += 0.08;
|
|
553
|
+
if (mode === 'entity_profile')
|
|
554
|
+
score += 0.08;
|
|
555
|
+
return Math.min(0.92, Number(score.toFixed(2)));
|
|
556
|
+
}
|
|
557
|
+
function followUpKind(value) {
|
|
558
|
+
return value && typeof value === 'object' ? String(value.kind ?? '') : undefined;
|
|
559
|
+
}
|
|
560
|
+
function uniqueEntities(items) {
|
|
561
|
+
const seen = new Set();
|
|
562
|
+
const out = [];
|
|
563
|
+
for (const item of items) {
|
|
564
|
+
const text = cleanEntityText(item.text);
|
|
565
|
+
const key = normalizeSearchText(text);
|
|
566
|
+
if (!key || seen.has(key))
|
|
567
|
+
continue;
|
|
568
|
+
seen.add(key);
|
|
569
|
+
out.push({ ...item, text });
|
|
570
|
+
}
|
|
571
|
+
return out;
|
|
572
|
+
}
|
|
573
|
+
function cleanEntityText(value) {
|
|
574
|
+
return value
|
|
575
|
+
.replace(/^(?:can\s+you\s+)?(?:research|reserach|show|find|provide|give|list|tell\s+me|run|execute|open)\s+/i, '')
|
|
576
|
+
.replace(/\b(profile|stats?|statistics|complete|full|details?|summary|with|and|by|for|from|in|on)\b.*$/i, '')
|
|
577
|
+
.replace(/[?.!,;:]+$/g, '')
|
|
578
|
+
.replace(/\s+/g, ' ')
|
|
579
|
+
.trim();
|
|
580
|
+
}
|
|
581
|
+
function isDirectKpiValueQuestion(lower) {
|
|
582
|
+
const asksForValue = /\b(what\s+(?:is|was|were|are)|how\s+(?:much|many)|show|report|calculate|give\s+me|tell\s+me)\b/.test(lower);
|
|
583
|
+
const metricLanguage = /\b(revenue|sales|arr|mrr|bookings|orders|customers|users|churn|retention|conversion|rate|count|total|points|goals|kpi|metric)\b/.test(lower);
|
|
584
|
+
const customGrain = /\b(by|break\s*down|breakdown|drill|compare|versus|vs\.?|segment|cohort|top|bottom|best|worst|highest|lowest|least|fewest|rank|ranking|most|why|changed?|driver|anomal|exception)\b/.test(lower);
|
|
585
|
+
return asksForValue && metricLanguage && !customGrain;
|
|
586
|
+
}
|
|
587
|
+
function tokenize(value) {
|
|
588
|
+
const tokens = new Set();
|
|
589
|
+
for (const raw of normalizeSearchText(value).split(/\s+/)) {
|
|
590
|
+
const normalized = normalizeTerm(raw);
|
|
591
|
+
if (!normalized || normalized.length < 2 || STOP_WORDS.has(normalized))
|
|
592
|
+
continue;
|
|
593
|
+
tokens.add(normalized);
|
|
594
|
+
}
|
|
595
|
+
return [...tokens];
|
|
596
|
+
}
|
|
597
|
+
function normalizeSearchText(value) {
|
|
598
|
+
return value.toLowerCase().replace(/[_-]+/g, ' ').replace(/[^a-z0-9@. ]+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
599
|
+
}
|
|
600
|
+
function normalizeTerm(value) {
|
|
601
|
+
const clean = normalizeSearchText(value);
|
|
602
|
+
if (clean.endsWith('ies') && clean.length > 4)
|
|
603
|
+
return `${clean.slice(0, -3)}y`;
|
|
604
|
+
if (clean.endsWith('s') && clean.length > 4)
|
|
605
|
+
return clean.slice(0, -1);
|
|
606
|
+
return clean;
|
|
607
|
+
}
|
|
608
|
+
function uniqueStrings(values) {
|
|
609
|
+
const seen = new Set();
|
|
610
|
+
const out = [];
|
|
611
|
+
for (const value of values) {
|
|
612
|
+
const clean = value.replace(/\s+/g, ' ').trim();
|
|
613
|
+
if (!clean)
|
|
614
|
+
continue;
|
|
615
|
+
const key = clean.toLowerCase();
|
|
616
|
+
if (seen.has(key))
|
|
617
|
+
continue;
|
|
618
|
+
seen.add(key);
|
|
619
|
+
out.push(clean);
|
|
620
|
+
}
|
|
621
|
+
return out;
|
|
622
|
+
}
|
|
623
|
+
function isCertifiedObject(object) {
|
|
624
|
+
return object.status === 'certified' || object.status === 'approved' || object.payload?.certification === 'certified';
|
|
625
|
+
}
|
|
626
|
+
function metadataString(value) {
|
|
627
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
628
|
+
}
|
|
629
|
+
function escapeRegExp(value) {
|
|
630
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
631
|
+
}
|
|
632
|
+
//# sourceMappingURL=analysis-planner.js.map
|