@duckcodeailabs/dql-agent 1.6.31 → 1.6.32
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/agent-run-engine.d.ts +42 -6
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +133 -79
- package/dist/agent-run-engine.js.map +1 -1
- package/dist/agent-run-gates.d.ts.map +1 -1
- package/dist/agent-run-gates.js +72 -5
- package/dist/agent-run-gates.js.map +1 -1
- package/dist/agent-run-planner.d.ts.map +1 -1
- package/dist/agent-run-planner.js +13 -0
- package/dist/agent-run-planner.js.map +1 -1
- package/dist/answer-loop.d.ts +110 -8
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +1679 -1299
- package/dist/answer-loop.js.map +1 -1
- package/dist/answer-shape.d.ts +16 -0
- package/dist/answer-shape.d.ts.map +1 -0
- package/dist/answer-shape.js +85 -0
- package/dist/answer-shape.js.map +1 -0
- package/dist/cascade/budgets.d.ts +101 -0
- package/dist/cascade/budgets.d.ts.map +1 -0
- package/dist/cascade/budgets.js +184 -0
- package/dist/cascade/budgets.js.map +1 -0
- package/dist/cascade/cascade.d.ts +86 -0
- package/dist/cascade/cascade.d.ts.map +1 -0
- package/dist/cascade/cascade.js +115 -0
- package/dist/cascade/cascade.js.map +1 -0
- package/dist/cascade/route-policy.d.ts +14 -0
- package/dist/cascade/route-policy.d.ts.map +1 -0
- package/dist/cascade/route-policy.js +48 -0
- package/dist/cascade/route-policy.js.map +1 -0
- package/dist/cascade/triage.d.ts +20 -0
- package/dist/cascade/triage.d.ts.map +1 -0
- package/dist/cascade/triage.js +25 -0
- package/dist/cascade/triage.js.map +1 -0
- package/dist/conversation/rolling-summary.d.ts +13 -0
- package/dist/conversation/rolling-summary.d.ts.map +1 -0
- package/dist/conversation/rolling-summary.js +43 -0
- package/dist/conversation/rolling-summary.js.map +1 -0
- package/dist/conversation/session-store.d.ts +110 -0
- package/dist/conversation/session-store.d.ts.map +1 -0
- package/dist/conversation/session-store.js +387 -0
- package/dist/conversation/session-store.js.map +1 -0
- package/dist/conversation/snapshot.d.ts +59 -0
- package/dist/conversation/snapshot.d.ts.map +1 -0
- package/dist/conversation/snapshot.js +153 -0
- package/dist/conversation/snapshot.js.map +1 -0
- package/dist/conversation/working-state.d.ts +49 -0
- package/dist/conversation/working-state.d.ts.map +1 -0
- package/dist/conversation/working-state.js +249 -0
- package/dist/conversation/working-state.js.map +1 -0
- package/dist/embeddings/provider.d.ts +62 -0
- package/dist/embeddings/provider.d.ts.map +1 -1
- package/dist/embeddings/provider.js +98 -0
- package/dist/embeddings/provider.js.map +1 -1
- package/dist/grounding/context-ledger.d.ts +30 -0
- package/dist/grounding/context-ledger.d.ts.map +1 -0
- package/dist/grounding/context-ledger.js +54 -0
- package/dist/grounding/context-ledger.js.map +1 -0
- package/dist/grounding/regrounding.d.ts +24 -0
- package/dist/grounding/regrounding.d.ts.map +1 -0
- package/dist/grounding/regrounding.js +219 -0
- package/dist/grounding/regrounding.js.map +1 -0
- package/dist/grounding/value-index.d.ts +32 -0
- package/dist/grounding/value-index.d.ts.map +1 -0
- package/dist/grounding/value-index.js +67 -0
- package/dist/grounding/value-index.js.map +1 -0
- package/dist/index.d.ts +45 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +102 -11
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +7 -0
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js +22 -1
- package/dist/intent-controller.js.map +1 -1
- package/dist/kg/build.d.ts.map +1 -1
- package/dist/kg/build.js +44 -17
- package/dist/kg/build.js.map +1 -1
- package/dist/kg/sqlite-fts.d.ts +3 -1
- package/dist/kg/sqlite-fts.d.ts.map +1 -1
- package/dist/kg/sqlite-fts.js +16 -2
- package/dist/kg/sqlite-fts.js.map +1 -1
- package/dist/kg/types.d.ts +23 -1
- package/dist/kg/types.d.ts.map +1 -1
- package/dist/memory/fts-query.d.ts +7 -0
- package/dist/memory/fts-query.d.ts.map +1 -0
- package/dist/memory/fts-query.js +36 -0
- package/dist/memory/fts-query.js.map +1 -0
- package/dist/memory/sqlite-memory.d.ts.map +1 -1
- package/dist/memory/sqlite-memory.js +1 -30
- package/dist/memory/sqlite-memory.js.map +1 -1
- package/dist/metadata/analysis-planner.d.ts +23 -0
- package/dist/metadata/analysis-planner.d.ts.map +1 -1
- package/dist/metadata/analysis-planner.js +432 -2
- package/dist/metadata/analysis-planner.js.map +1 -1
- package/dist/metadata/block-fit.d.ts +24 -0
- package/dist/metadata/block-fit.d.ts.map +1 -0
- package/dist/metadata/block-fit.js +373 -0
- package/dist/metadata/block-fit.js.map +1 -0
- package/dist/metadata/catalog.d.ts +72 -3
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +1452 -72
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/drafts.d.ts +25 -0
- package/dist/metadata/drafts.d.ts.map +1 -1
- package/dist/metadata/drafts.js +112 -2
- package/dist/metadata/drafts.js.map +1 -1
- package/dist/metadata/metric-match.d.ts +55 -6
- package/dist/metadata/metric-match.d.ts.map +1 -1
- package/dist/metadata/metric-match.js +158 -13
- package/dist/metadata/metric-match.js.map +1 -1
- package/dist/metadata/sql-context-validation.d.ts +13 -1
- package/dist/metadata/sql-context-validation.d.ts.map +1 -1
- package/dist/metadata/sql-context-validation.js +172 -19
- package/dist/metadata/sql-context-validation.js.map +1 -1
- package/dist/metadata/sql-retrieval.d.ts +3 -2
- package/dist/metadata/sql-retrieval.d.ts.map +1 -1
- package/dist/metadata/sql-retrieval.js +6 -4
- package/dist/metadata/sql-retrieval.js.map +1 -1
- package/dist/providers/claude.d.ts +2 -1
- package/dist/providers/claude.d.ts.map +1 -1
- package/dist/providers/claude.js +95 -0
- package/dist/providers/claude.js.map +1 -1
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai.d.ts +2 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +143 -0
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/tool-output.d.ts +2 -0
- package/dist/providers/tool-output.d.ts.map +1 -0
- package/dist/providers/tool-output.js +22 -0
- package/dist/providers/tool-output.js.map +1 -0
- package/dist/providers/types.d.ts +23 -0
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/types.js.map +1 -1
- package/dist/semantic-bridge/compose.d.ts +89 -0
- package/dist/semantic-bridge/compose.d.ts.map +1 -0
- package/dist/semantic-bridge/compose.js +528 -0
- package/dist/semantic-bridge/compose.js.map +1 -0
- package/dist/semantic-bridge/member-select.d.ts +23 -0
- package/dist/semantic-bridge/member-select.d.ts.map +1 -0
- package/dist/semantic-bridge/member-select.js +106 -0
- package/dist/semantic-bridge/member-select.js.map +1 -0
- package/dist/skills/defaults.d.ts.map +1 -1
- package/dist/skills/defaults.js +5 -0
- package/dist/skills/defaults.js.map +1 -1
- package/dist/synthesize.d.ts +26 -0
- package/dist/synthesize.d.ts.map +1 -1
- package/dist/synthesize.js +72 -3
- package/dist/synthesize.js.map +1 -1
- package/dist/tools/registry.d.ts +15 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +692 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/trust/stamp.d.ts +26 -0
- package/dist/trust/stamp.d.ts.map +1 -0
- package/dist/trust/stamp.js +33 -0
- package/dist/trust/stamp.js.map +1 -0
- package/package.json +4 -4
|
@@ -26,7 +26,10 @@ export function buildAnalysisQuestionPlan(question, followUp) {
|
|
|
26
26
|
const normalizedQuestion = normalizeSearchText(cleanQuestion);
|
|
27
27
|
const lower = cleanQuestion.toLowerCase();
|
|
28
28
|
const entities = extractEntities(cleanQuestion);
|
|
29
|
-
const metricTerms =
|
|
29
|
+
const metricTerms = uniqueStrings([
|
|
30
|
+
...extractMetricTerms(cleanQuestion),
|
|
31
|
+
...extractFollowUpMetricTerms(followUp, lower),
|
|
32
|
+
]);
|
|
30
33
|
const dimensionTerms = extractDimensionTerms(cleanQuestion);
|
|
31
34
|
const filterTerms = extractFilterTerms(cleanQuestion, entities);
|
|
32
35
|
const timeTerms = extractTimeTerms(cleanQuestion);
|
|
@@ -36,6 +39,15 @@ export function buildAnalysisQuestionPlan(question, followUp) {
|
|
|
36
39
|
const shouldConsiderCertifiedExact = certifiedExactIsPlausible(mode, entities);
|
|
37
40
|
const needsGeneratedSql = generatedSqlIsLikely(mode, shouldConsiderCertifiedExact);
|
|
38
41
|
const needsResearchWorkspace = researchWorkspaceIsLikely(mode, lower);
|
|
42
|
+
const requestedShape = buildRequestedAnswerShape(cleanQuestion, {
|
|
43
|
+
lower,
|
|
44
|
+
mode,
|
|
45
|
+
metricTerms,
|
|
46
|
+
dimensionTerms,
|
|
47
|
+
filterTerms,
|
|
48
|
+
timeTerms,
|
|
49
|
+
followUp,
|
|
50
|
+
});
|
|
39
51
|
const searchTerms = uniqueStrings([
|
|
40
52
|
...tokenize(cleanQuestion),
|
|
41
53
|
...entities.flatMap((entity) => tokenize(entity.text)),
|
|
@@ -72,6 +84,7 @@ export function buildAnalysisQuestionPlan(question, followUp) {
|
|
|
72
84
|
needsResearchWorkspace,
|
|
73
85
|
searchQueries,
|
|
74
86
|
searchTerms,
|
|
87
|
+
requestedShape,
|
|
75
88
|
confidence: planConfidence(mode, entities, metricTerms, dimensionTerms),
|
|
76
89
|
reasons,
|
|
77
90
|
};
|
|
@@ -209,6 +222,12 @@ export function scoreAllowedSqlRelationWithAnalysisPlan(relation, plan) {
|
|
|
209
222
|
: 0;
|
|
210
223
|
const usabilityBonus = generatedSqlUsabilityScore(relation, plan);
|
|
211
224
|
const columnShapeBonus = relationColumnShapeScore(relation, plan);
|
|
225
|
+
const semanticColumnBonus = semanticColumnMapScore(relation, plan);
|
|
226
|
+
const incompleteSourceShapePenalty = relation.source.includes('certified source SQL') &&
|
|
227
|
+
sourceShapeMustCoverRequestedOutputs(plan) &&
|
|
228
|
+
semanticColumnBonus.missingRequired.length > 0
|
|
229
|
+
? Math.min(42, semanticColumnBonus.missingRequired.length * 34)
|
|
230
|
+
: 0;
|
|
212
231
|
const score = Number((search.score * 3 +
|
|
213
232
|
metrics.score * 8 +
|
|
214
233
|
dimensions.score * 6 +
|
|
@@ -217,6 +236,8 @@ export function scoreAllowedSqlRelationWithAnalysisPlan(relation, plan) {
|
|
|
217
236
|
sourceBonus +
|
|
218
237
|
usabilityBonus +
|
|
219
238
|
columnShapeBonus +
|
|
239
|
+
semanticColumnBonus.score +
|
|
240
|
+
-incompleteSourceShapePenalty +
|
|
220
241
|
Math.min(relation.columns.length, 24) * 0.2).toFixed(3));
|
|
221
242
|
const reasons = [
|
|
222
243
|
search.matched.length ? `analysis terms matched: ${search.matched.slice(0, 5).join(', ')}` : '',
|
|
@@ -228,6 +249,8 @@ export function scoreAllowedSqlRelationWithAnalysisPlan(relation, plan) {
|
|
|
228
249
|
usabilityBonus > 0 ? 'relation has usable columns for generated SQL' : '',
|
|
229
250
|
usabilityBonus < 0 ? 'relation has no inspected/projected columns for generated SQL' : '',
|
|
230
251
|
columnShapeBonus > 0 ? 'columns match requested analytical shape' : '',
|
|
252
|
+
semanticColumnBonus.matched.length ? `semantic column map matched: ${semanticColumnBonus.matched.slice(0, 6).join(', ')}` : '',
|
|
253
|
+
incompleteSourceShapePenalty > 0 ? `certified source SQL shape is missing requested output(s): ${semanticColumnBonus.missingRequired.join(', ')}` : '',
|
|
231
254
|
relation.columns.length > 0 ? `${relation.columns.length} inspected/projected columns` : '',
|
|
232
255
|
].filter(Boolean);
|
|
233
256
|
return {
|
|
@@ -266,6 +289,152 @@ function relationColumnShapeScore(relation, plan) {
|
|
|
266
289
|
}
|
|
267
290
|
return score;
|
|
268
291
|
}
|
|
292
|
+
function semanticColumnMapScore(relation, plan) {
|
|
293
|
+
if (relation.columns.length === 0)
|
|
294
|
+
return { score: 0, matched: [], missingRequired: [] };
|
|
295
|
+
const requiredConcepts = uniqueStrings(plan.requestedShape.requiredOutputs.map(canonicalColumnConcept).filter(Boolean));
|
|
296
|
+
const requestedOutputs = uniqueStrings([
|
|
297
|
+
...requiredConcepts,
|
|
298
|
+
...plan.requestedShape.dimensions,
|
|
299
|
+
...plan.requestedShape.measures,
|
|
300
|
+
...plan.dimensionTerms,
|
|
301
|
+
...plan.metricTerms,
|
|
302
|
+
].map(canonicalColumnConcept).filter(Boolean));
|
|
303
|
+
const matched = [];
|
|
304
|
+
const missingRequired = [];
|
|
305
|
+
let score = 0;
|
|
306
|
+
for (const concept of requestedOutputs) {
|
|
307
|
+
const best = bestColumnConceptScore(concept, relation.columns);
|
|
308
|
+
if (best.score <= 0) {
|
|
309
|
+
if (requiredConcepts.includes(concept)) {
|
|
310
|
+
missingRequired.push(concept);
|
|
311
|
+
score -= 12;
|
|
312
|
+
}
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
matched.push(`${concept}->${best.column}`);
|
|
316
|
+
score += best.score * (requiredConcepts.includes(concept) ? 1.35 : 1);
|
|
317
|
+
}
|
|
318
|
+
const capped = Math.max(-36, Math.min(score, plan.needsGeneratedSql ? 44 : 28));
|
|
319
|
+
return { score: capped, matched: uniqueStrings(matched), missingRequired: uniqueStrings(missingRequired) };
|
|
320
|
+
}
|
|
321
|
+
function sourceShapeMustCoverRequestedOutputs(plan) {
|
|
322
|
+
return plan.mode === 'ranking' ||
|
|
323
|
+
plan.mode === 'driver_breakdown' ||
|
|
324
|
+
plan.mode === 'comparison' ||
|
|
325
|
+
plan.mode === 'trend' ||
|
|
326
|
+
plan.mode === 'general_analysis';
|
|
327
|
+
}
|
|
328
|
+
function canonicalColumnConcept(value) {
|
|
329
|
+
return normalizeTerm(value)
|
|
330
|
+
.replace(/\bname\b$/i, '')
|
|
331
|
+
.replace(/\s+/g, ' ')
|
|
332
|
+
.trim();
|
|
333
|
+
}
|
|
334
|
+
function bestColumnConceptScore(concept, columns) {
|
|
335
|
+
let best = { score: 0, column: '' };
|
|
336
|
+
for (const column of columns) {
|
|
337
|
+
const score = columnConceptScore(concept, column);
|
|
338
|
+
if (score > best.score)
|
|
339
|
+
best = { score, column: column.name };
|
|
340
|
+
}
|
|
341
|
+
return best;
|
|
342
|
+
}
|
|
343
|
+
function columnConceptScore(concept, column) {
|
|
344
|
+
const normalizedConcept = normalizeTerm(concept.replace(/_name$/, ''));
|
|
345
|
+
if (!normalizedConcept)
|
|
346
|
+
return 0;
|
|
347
|
+
const columnName = normalizeSearchText(column.name);
|
|
348
|
+
const columnTokens = tokenize(column.name);
|
|
349
|
+
const columnText = normalizeSearchText([
|
|
350
|
+
column.name,
|
|
351
|
+
column.description ?? '',
|
|
352
|
+
].join(' '));
|
|
353
|
+
const aliases = semanticColumnAliases(normalizedConcept);
|
|
354
|
+
let score = 0;
|
|
355
|
+
if (columnName === normalizedConcept || columnName === `${normalizedConcept} name`)
|
|
356
|
+
score += 12;
|
|
357
|
+
if (columnTokens.includes(normalizedConcept))
|
|
358
|
+
score += 9;
|
|
359
|
+
for (const alias of aliases) {
|
|
360
|
+
const aliasText = normalizeSearchText(alias);
|
|
361
|
+
const aliasTokens = tokenize(alias).filter((token) => !GENERIC_COLUMN_ALIAS_TOKENS.has(token));
|
|
362
|
+
if (!aliasText)
|
|
363
|
+
continue;
|
|
364
|
+
if (columnName === aliasText)
|
|
365
|
+
score += 11;
|
|
366
|
+
else if (columnText.includes(aliasText))
|
|
367
|
+
score += 7;
|
|
368
|
+
else if (aliasTokens.some((token) => columnTokens.includes(token)))
|
|
369
|
+
score += 5;
|
|
370
|
+
}
|
|
371
|
+
if (lineageAliasesIncludeConcept(column.description, normalizedConcept, aliases))
|
|
372
|
+
score += 12;
|
|
373
|
+
if (isCertifiedSourceShapeDescription(column.description))
|
|
374
|
+
score -= 16;
|
|
375
|
+
if (isMetricConcept(normalizedConcept) && isIdentifierLikeColumn(column.name))
|
|
376
|
+
score -= 8;
|
|
377
|
+
if (isDimensionConcept(normalizedConcept) && isMetricLikeColumn(column.name))
|
|
378
|
+
score -= 6;
|
|
379
|
+
return Math.max(0, score);
|
|
380
|
+
}
|
|
381
|
+
const GENERIC_COLUMN_ALIAS_TOKENS = new Set(['name', 'title', 'type', 'value', 'total']);
|
|
382
|
+
function lineageAliasesIncludeConcept(description, concept, aliases) {
|
|
383
|
+
const match = /governed aliases from lineage:\s*([^.]*)/i.exec(description ?? '');
|
|
384
|
+
if (!match?.[1])
|
|
385
|
+
return false;
|
|
386
|
+
const aliasText = normalizeSearchText(match[1]);
|
|
387
|
+
const candidates = [concept, ...aliases].map(normalizeSearchText).filter(Boolean);
|
|
388
|
+
return candidates.some((candidate) => aliasText === candidate ||
|
|
389
|
+
aliasText.split(/\s*,\s*/).some((alias) => normalizeSearchText(alias) === candidate));
|
|
390
|
+
}
|
|
391
|
+
function isCertifiedSourceShapeDescription(description) {
|
|
392
|
+
return /projected by certified source sql shape/i.test(description ?? '');
|
|
393
|
+
}
|
|
394
|
+
function semanticColumnAliases(concept) {
|
|
395
|
+
switch (concept) {
|
|
396
|
+
case 'revenue':
|
|
397
|
+
case 'sale':
|
|
398
|
+
case 'sales':
|
|
399
|
+
return ['revenue', 'sales', 'amount', 'gross amount', 'net amount', 'price', 'product price', 'order amount', 'total'];
|
|
400
|
+
case 'usage':
|
|
401
|
+
return ['usage', 'use', 'consumption', 'activity', 'events', 'sessions', 'volume'];
|
|
402
|
+
case 'order':
|
|
403
|
+
case 'orders':
|
|
404
|
+
return ['order count', 'orders', 'order total', 'count lifetime orders'];
|
|
405
|
+
case 'product':
|
|
406
|
+
return ['product', 'product name', 'product title', 'sku', 'item', 'item name'];
|
|
407
|
+
case 'category':
|
|
408
|
+
return ['category', 'category name', 'product category', 'product type', 'type', 'class'];
|
|
409
|
+
case 'customer':
|
|
410
|
+
return ['customer', 'customer name', 'buyer', 'client', 'account name', 'full name'];
|
|
411
|
+
case 'segment':
|
|
412
|
+
return ['segment', 'customer segment', 'market segment'];
|
|
413
|
+
case 'region':
|
|
414
|
+
case 'market':
|
|
415
|
+
return ['region', 'market', 'geo', 'location', 'country', 'state'];
|
|
416
|
+
case 'player':
|
|
417
|
+
return ['player', 'player name', 'athlete', 'athlete name'];
|
|
418
|
+
case 'month':
|
|
419
|
+
case 'week':
|
|
420
|
+
case 'year':
|
|
421
|
+
return [concept, `${concept} date`, `${concept} start`, 'date', 'period'];
|
|
422
|
+
default:
|
|
423
|
+
return [concept.replace(/_/g, ' ')];
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
function isMetricConcept(concept) {
|
|
427
|
+
return METRIC_WORDS.includes(concept) || ['sale', 'sales', 'order', 'orders'].includes(concept);
|
|
428
|
+
}
|
|
429
|
+
function isDimensionConcept(concept) {
|
|
430
|
+
return DIMENSION_WORDS.includes(concept);
|
|
431
|
+
}
|
|
432
|
+
function isIdentifierLikeColumn(column) {
|
|
433
|
+
return /\b(id|key|uuid|code|hash)\b/i.test(column.replace(/_/g, ' '));
|
|
434
|
+
}
|
|
435
|
+
function isMetricLikeColumn(column) {
|
|
436
|
+
return /\b(revenue|sales|amount|price|spend|cost|total|count|score|points?|quantity|value|rate|volume)\b/i.test(column.replace(/_/g, ' '));
|
|
437
|
+
}
|
|
269
438
|
function inferQuestionMode(input) {
|
|
270
439
|
const { lower } = input;
|
|
271
440
|
if (followUpKind(input.followUp) === 'drilldown')
|
|
@@ -375,6 +544,12 @@ function extractMetricTerms(question) {
|
|
|
375
544
|
terms.add('score');
|
|
376
545
|
terms.add('scoring');
|
|
377
546
|
}
|
|
547
|
+
if (/\b(how many|number of)\b/i.test(lower)) {
|
|
548
|
+
terms.add('count');
|
|
549
|
+
}
|
|
550
|
+
if (/\bhow much\b/i.test(lower)) {
|
|
551
|
+
terms.add('amount');
|
|
552
|
+
}
|
|
378
553
|
for (const word of METRIC_WORDS) {
|
|
379
554
|
if (new RegExp(`\\b${escapeRegExp(word)}s?\\b`, 'i').test(lower))
|
|
380
555
|
terms.add(normalizeTerm(word));
|
|
@@ -388,8 +563,10 @@ function extractMetricTerms(question) {
|
|
|
388
563
|
function extractDimensionTerms(question) {
|
|
389
564
|
const lower = question.toLowerCase();
|
|
390
565
|
const terms = new Set();
|
|
566
|
+
if (/\b(?:cusomers?|custmers?|costomers?|clients?|buyers?)\b/i.test(lower))
|
|
567
|
+
terms.add('customer');
|
|
391
568
|
for (const word of DIMENSION_WORDS) {
|
|
392
|
-
if (new RegExp(`\\b
|
|
569
|
+
if (new RegExp(`\\b(?:${escapeRegExp(word)}|${escapeRegExp(pluralizeDimensionWord(word))})\\b`, 'i').test(lower))
|
|
393
570
|
terms.add(normalizeTerm(word));
|
|
394
571
|
}
|
|
395
572
|
for (const match of lower.matchAll(/\bby\s+([a-z][a-z0-9_ -]{1,40})/g)) {
|
|
@@ -399,7 +576,15 @@ function extractDimensionTerms(question) {
|
|
|
399
576
|
}
|
|
400
577
|
return uniqueStrings([...terms]).slice(0, 16);
|
|
401
578
|
}
|
|
579
|
+
function pluralizeDimensionWord(word) {
|
|
580
|
+
if (word.endsWith('y'))
|
|
581
|
+
return `${word.slice(0, -1)}ies`;
|
|
582
|
+
return `${word}s`;
|
|
583
|
+
}
|
|
402
584
|
function extractFilterTerms(question, entities) {
|
|
585
|
+
// Filter VALUES come from the question's own entities and time phrases only.
|
|
586
|
+
// (Real value binding happens later against the runtime value index / sampled
|
|
587
|
+
// column values — never from a hard-coded project-specific vocabulary.)
|
|
403
588
|
return uniqueStrings([
|
|
404
589
|
...entities.flatMap((entity) => tokenize(entity.text)),
|
|
405
590
|
...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()),
|
|
@@ -416,6 +601,249 @@ function extractTimeTerms(question) {
|
|
|
416
601
|
}
|
|
417
602
|
return uniqueStrings(terms).slice(0, 12);
|
|
418
603
|
}
|
|
604
|
+
function extractFollowUpMetricTerms(followUp, lowerQuestion) {
|
|
605
|
+
if (!followUp || typeof followUp !== 'object' || Array.isArray(followUp))
|
|
606
|
+
return [];
|
|
607
|
+
const record = followUp;
|
|
608
|
+
// Advisory 'contextual' carry must not inject prior measures into a genuinely-new
|
|
609
|
+
// question's plan (it would bias retrieval and the block-fit gate toward the old
|
|
610
|
+
// topic). Carry prior measures only when the question textually refers back.
|
|
611
|
+
if (record.kind === 'contextual' && lowerQuestion !== undefined
|
|
612
|
+
&& !/\b(these|those|that|them|this|same|prior|previous|above)\b/.test(lowerQuestion)) {
|
|
613
|
+
return [];
|
|
614
|
+
}
|
|
615
|
+
return uniqueStrings([
|
|
616
|
+
...cleanStringArray(record.priorMeasures),
|
|
617
|
+
...cleanStringArray(record.priorResultColumns).filter((column) => METRIC_WORDS.some((word) => new RegExp(`\\b${word}\\b`, 'i').test(column.replace(/_/g, ' ')))),
|
|
618
|
+
]).slice(0, 8);
|
|
619
|
+
}
|
|
620
|
+
function cleanStringArray(value) {
|
|
621
|
+
if (!Array.isArray(value))
|
|
622
|
+
return [];
|
|
623
|
+
return value
|
|
624
|
+
.map((item) => typeof item === 'string' ? item.trim() : '')
|
|
625
|
+
.filter(Boolean);
|
|
626
|
+
}
|
|
627
|
+
function buildRequestedAnswerShape(question, input) {
|
|
628
|
+
const topN = parseTopN(question);
|
|
629
|
+
const followUpReferences = extractFollowUpReferences(question, input.followUp);
|
|
630
|
+
const rawDimensions = uniqueStrings([
|
|
631
|
+
...input.dimensionTerms.map(canonicalShapeTerm),
|
|
632
|
+
...followUpDimensionsForRequestedShape(input.followUp, followUpReferences),
|
|
633
|
+
].filter(Boolean));
|
|
634
|
+
const dimensions = scalarValueQuestionUsesEntitiesAsMeasures(input.mode, input.lower)
|
|
635
|
+
? []
|
|
636
|
+
: rawDimensions;
|
|
637
|
+
const measures = uniqueStrings(input.metricTerms.map(canonicalShapeTerm).filter(Boolean));
|
|
638
|
+
const requiredOutputs = extractRequiredOutputs(question, dimensions, measures);
|
|
639
|
+
const filters = uniqueStrings([
|
|
640
|
+
...input.filterTerms,
|
|
641
|
+
...followUpFiltersForRequestedShape(input.followUp, followUpReferences),
|
|
642
|
+
]);
|
|
643
|
+
const ambiguities = /\bimpact(?:ed|s|ing)?\b/i.test(question)
|
|
644
|
+
? [{
|
|
645
|
+
term: 'impacted',
|
|
646
|
+
defaultInterpretation: 'highest contribution to the requested metric',
|
|
647
|
+
requiresClarification: false,
|
|
648
|
+
}]
|
|
649
|
+
: [];
|
|
650
|
+
return {
|
|
651
|
+
grain: inferRequestedShapeGrain(dimensions, input.timeTerms),
|
|
652
|
+
dimensions,
|
|
653
|
+
measures,
|
|
654
|
+
requiredOutputs,
|
|
655
|
+
filters,
|
|
656
|
+
...(topN ? { topN } : {}),
|
|
657
|
+
rankingDirection: rankingDirection(question),
|
|
658
|
+
followUpReferences,
|
|
659
|
+
ambiguities,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
function scalarValueQuestionUsesEntitiesAsMeasures(mode, lower) {
|
|
663
|
+
if (mode !== 'exact_lookup' && mode !== 'general_analysis')
|
|
664
|
+
return false;
|
|
665
|
+
return /\b(how many|how much|what is|what was|total|count|number of)\b/.test(lower)
|
|
666
|
+
&& !/\b(by|per|each|every|top|bottom|rank|ranking|list|which|who|break\s*down|breakdown|split|segment|trend|over time)\b/.test(lower);
|
|
667
|
+
}
|
|
668
|
+
function parseTopN(question) {
|
|
669
|
+
const lower = question.toLowerCase();
|
|
670
|
+
const numeric = lower.match(/\b(?:top|bottom|first|last)\s+(\d{1,3})\b/);
|
|
671
|
+
const n = numeric ? Number(numeric[1]) : wordNumberFromTopN(lower);
|
|
672
|
+
if (!n || n <= 0)
|
|
673
|
+
return undefined;
|
|
674
|
+
const scope = /\b(?:per|for each|within each)\s+\w+/i.test(lower)
|
|
675
|
+
|| /\bby\s+(?:category|segment|region|channel|product|customer|account|user|month|week|year)\b/i.test(lower)
|
|
676
|
+
? 'per_group'
|
|
677
|
+
: 'overall';
|
|
678
|
+
return { n, scope };
|
|
679
|
+
}
|
|
680
|
+
function wordNumberFromTopN(lower) {
|
|
681
|
+
const words = {
|
|
682
|
+
one: 1,
|
|
683
|
+
two: 2,
|
|
684
|
+
three: 3,
|
|
685
|
+
four: 4,
|
|
686
|
+
five: 5,
|
|
687
|
+
six: 6,
|
|
688
|
+
seven: 7,
|
|
689
|
+
eight: 8,
|
|
690
|
+
nine: 9,
|
|
691
|
+
ten: 10,
|
|
692
|
+
};
|
|
693
|
+
const match = lower.match(/\b(?:top|bottom|first|last)\s+(one|two|three|four|five|six|seven|eight|nine|ten)\b/);
|
|
694
|
+
return match ? words[match[1]] : undefined;
|
|
695
|
+
}
|
|
696
|
+
function extractRequiredOutputs(question, dimensions, measures) {
|
|
697
|
+
const lower = question.toLowerCase();
|
|
698
|
+
const outputs = new Set();
|
|
699
|
+
for (const dim of dimensions)
|
|
700
|
+
outputs.add(dim);
|
|
701
|
+
for (const measure of measures)
|
|
702
|
+
outputs.add(measure);
|
|
703
|
+
if (/\bproduct\s+name\b/i.test(lower))
|
|
704
|
+
outputs.add('product_name');
|
|
705
|
+
if (/\bcustomer\s+name\b/i.test(lower))
|
|
706
|
+
outputs.add('customer_name');
|
|
707
|
+
if (/\bcategory\s+name\b/i.test(lower))
|
|
708
|
+
outputs.add('category_name');
|
|
709
|
+
if (/\b(?:name|names)\b/i.test(lower)) {
|
|
710
|
+
if (dimensions.includes('product'))
|
|
711
|
+
outputs.add('product_name');
|
|
712
|
+
if (dimensions.includes('customer'))
|
|
713
|
+
outputs.add('customer_name');
|
|
714
|
+
if (dimensions.includes('category'))
|
|
715
|
+
outputs.add('category_name');
|
|
716
|
+
}
|
|
717
|
+
return uniqueStrings([...outputs]).slice(0, 24);
|
|
718
|
+
}
|
|
719
|
+
function extractFollowUpReferences(question, followUp) {
|
|
720
|
+
const refs = [];
|
|
721
|
+
const lower = question.toLowerCase();
|
|
722
|
+
const record = followUpRecord(followUp);
|
|
723
|
+
const hasFollowUp = Boolean(record);
|
|
724
|
+
for (const match of lower.matchAll(/\b(these|those|that|same|prior|previous)\s+([a-z][a-z0-9_ -]{1,30})\b/g)) {
|
|
725
|
+
const phrase = match[0];
|
|
726
|
+
const noun = canonicalShapeTerm(match[2]);
|
|
727
|
+
let kind = 'ambiguous';
|
|
728
|
+
if (/period|date|day|week|month|quarter|year|time/.test(noun))
|
|
729
|
+
kind = 'prior_timeframe';
|
|
730
|
+
else if (hasFollowUp && /\b(category|product|customer|account|user|region|segment|channel)\b/.test(noun))
|
|
731
|
+
kind = 'prior_dimension_values';
|
|
732
|
+
else if (hasFollowUp)
|
|
733
|
+
kind = 'prior_entities';
|
|
734
|
+
refs.push({
|
|
735
|
+
phrase,
|
|
736
|
+
kind,
|
|
737
|
+
...resolvedFollowUpValues(record, noun, kind),
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
if (refs.length === 0 && record) {
|
|
741
|
+
const bare = lower.match(/\b(these|those|that|them|same)\b/);
|
|
742
|
+
const dimension = bare ? singleFollowUpDimension(record) : undefined;
|
|
743
|
+
if (bare && dimension) {
|
|
744
|
+
refs.push({
|
|
745
|
+
phrase: bare[0],
|
|
746
|
+
kind: 'prior_dimension_values',
|
|
747
|
+
...resolvedFollowUpValues(record, dimension, 'prior_dimension_values'),
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return refs.slice(0, 8);
|
|
752
|
+
}
|
|
753
|
+
function followUpDimensionsForRequestedShape(followUp, refs) {
|
|
754
|
+
const record = followUpRecord(followUp);
|
|
755
|
+
if (!record || refs.every((ref) => ref.kind !== 'prior_dimension_values'))
|
|
756
|
+
return [];
|
|
757
|
+
const explicit = cleanStringArray(record.dimensions).map(contextDimensionTerm).filter(Boolean);
|
|
758
|
+
if (explicit.length > 0)
|
|
759
|
+
return uniqueStrings(explicit);
|
|
760
|
+
const single = singleFollowUpDimension(record);
|
|
761
|
+
return single ? [single] : [];
|
|
762
|
+
}
|
|
763
|
+
function followUpFiltersForRequestedShape(followUp, refs) {
|
|
764
|
+
const record = followUpRecord(followUp);
|
|
765
|
+
if (!record || refs.every((ref) => ref.kind !== 'prior_dimension_values'))
|
|
766
|
+
return [];
|
|
767
|
+
return uniqueStrings([
|
|
768
|
+
...cleanStringArray(record.filters),
|
|
769
|
+
...refs.flatMap((ref) => ref.resolvedValues ?? []),
|
|
770
|
+
]);
|
|
771
|
+
}
|
|
772
|
+
function resolvedFollowUpValues(record, dimension, kind) {
|
|
773
|
+
if (!record || kind !== 'prior_dimension_values')
|
|
774
|
+
return {};
|
|
775
|
+
const values = followUpValuesForDimension(record, dimension);
|
|
776
|
+
return values.length > 0 ? { resolvedValues: values } : {};
|
|
777
|
+
}
|
|
778
|
+
function followUpValuesForDimension(record, dimension) {
|
|
779
|
+
const valuesByDimension = cleanStringRecord(record.priorResultValues);
|
|
780
|
+
const canonicalDimension = dimension ? contextDimensionTerm(dimension) : singleFollowUpDimension(record);
|
|
781
|
+
const values = canonicalDimension
|
|
782
|
+
? valuesByDimension[canonicalDimension] ?? valuesByDimension[`${canonicalDimension}_name`] ?? []
|
|
783
|
+
: [];
|
|
784
|
+
return uniqueStrings([
|
|
785
|
+
...values,
|
|
786
|
+
...cleanStringArray(record.filters),
|
|
787
|
+
]).slice(0, 24);
|
|
788
|
+
}
|
|
789
|
+
function singleFollowUpDimension(record) {
|
|
790
|
+
const explicit = uniqueStrings(cleanStringArray(record.dimensions).map(contextDimensionTerm).filter(Boolean));
|
|
791
|
+
if (explicit.length === 1)
|
|
792
|
+
return explicit[0];
|
|
793
|
+
if (explicit.length > 1)
|
|
794
|
+
return undefined;
|
|
795
|
+
const valuesByDimension = cleanStringRecord(record.priorResultValues);
|
|
796
|
+
const inferred = uniqueStrings(Object.keys(valuesByDimension)
|
|
797
|
+
.map(contextDimensionTerm)
|
|
798
|
+
.filter((term) => term && !METRIC_WORDS.includes(term)));
|
|
799
|
+
return inferred.length === 1 ? inferred[0] : undefined;
|
|
800
|
+
}
|
|
801
|
+
function contextDimensionTerm(value) {
|
|
802
|
+
const term = canonicalShapeTerm(value);
|
|
803
|
+
if (term.includes('category'))
|
|
804
|
+
return 'category';
|
|
805
|
+
if (term.includes('product'))
|
|
806
|
+
return 'product';
|
|
807
|
+
if (term.includes('customer'))
|
|
808
|
+
return 'customer';
|
|
809
|
+
if (term.includes('account'))
|
|
810
|
+
return 'account';
|
|
811
|
+
if (term.includes('user'))
|
|
812
|
+
return 'user';
|
|
813
|
+
if (term.includes('region'))
|
|
814
|
+
return 'region';
|
|
815
|
+
if (term.includes('segment'))
|
|
816
|
+
return 'segment';
|
|
817
|
+
if (term.includes('channel'))
|
|
818
|
+
return 'channel';
|
|
819
|
+
return term;
|
|
820
|
+
}
|
|
821
|
+
function cleanStringRecord(value) {
|
|
822
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
823
|
+
return {};
|
|
824
|
+
const out = {};
|
|
825
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
826
|
+
const cleanKey = contextDimensionTerm(key);
|
|
827
|
+
const values = cleanStringArray(raw).slice(0, 24);
|
|
828
|
+
if (cleanKey && values.length > 0)
|
|
829
|
+
out[cleanKey] = values;
|
|
830
|
+
}
|
|
831
|
+
return out;
|
|
832
|
+
}
|
|
833
|
+
function followUpRecord(value) {
|
|
834
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
835
|
+
? value
|
|
836
|
+
: undefined;
|
|
837
|
+
}
|
|
838
|
+
function inferRequestedShapeGrain(dimensions, timeTerms) {
|
|
839
|
+
if (dimensions.length > 0)
|
|
840
|
+
return dimensions[0];
|
|
841
|
+
const time = timeTerms.find((term) => /\b(hour|day|week|month|quarter|year|season|period|date)\b/i.test(term));
|
|
842
|
+
return time ? canonicalShapeTerm(time) : undefined;
|
|
843
|
+
}
|
|
844
|
+
function canonicalShapeTerm(value) {
|
|
845
|
+
return normalizeTerm(value).replace(/\s+/g, '_');
|
|
846
|
+
}
|
|
419
847
|
function buildSearchQueries(input) {
|
|
420
848
|
const entityText = input.entities.map((entity) => entity.text).join(' ');
|
|
421
849
|
const profileTerms = input.mode === 'entity_profile' ? PROFILE_WORDS.join(' ') : '';
|
|
@@ -599,6 +1027,8 @@ function normalizeSearchText(value) {
|
|
|
599
1027
|
}
|
|
600
1028
|
function normalizeTerm(value) {
|
|
601
1029
|
const clean = normalizeSearchText(value);
|
|
1030
|
+
if (/^(cusomer|custmer|costomer|client|buyer)s?$/.test(clean))
|
|
1031
|
+
return 'customer';
|
|
602
1032
|
if (clean.endsWith('ies') && clean.length > 4)
|
|
603
1033
|
return `${clean.slice(0, -3)}y`;
|
|
604
1034
|
if (clean.endsWith('s') && clean.length > 4)
|