@duckcodeailabs/dql-agent 1.9.4 → 1.10.0
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 +13 -0
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +59 -1
- package/dist/agent-run-engine.js.map +1 -1
- package/dist/answer-loop.d.ts +34 -1
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +490 -57
- package/dist/answer-loop.js.map +1 -1
- package/dist/embeddings/provider.d.ts +1 -1
- package/dist/embeddings/provider.d.ts.map +1 -1
- package/dist/embeddings/provider.js.map +1 -1
- package/dist/governed-relational-compiler.d.ts +138 -0
- package/dist/governed-relational-compiler.d.ts.map +1 -0
- package/dist/governed-relational-compiler.js +315 -0
- package/dist/governed-relational-compiler.js.map +1 -0
- package/dist/index.d.ts +13 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +5 -0
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js.map +1 -1
- package/dist/kg/build.d.ts.map +1 -1
- package/dist/kg/build.js +81 -1
- package/dist/kg/build.js.map +1 -1
- package/dist/meaning-resolution.d.ts +4 -0
- package/dist/meaning-resolution.d.ts.map +1 -1
- package/dist/meaning-resolution.js +14 -4
- package/dist/meaning-resolution.js.map +1 -1
- package/dist/metadata/analysis-planner.d.ts.map +1 -1
- package/dist/metadata/analysis-planner.js +34 -6
- package/dist/metadata/analysis-planner.js.map +1 -1
- package/dist/metadata/catalog.d.ts +83 -2
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +381 -28
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/meaning-evidence.d.ts +10 -1
- package/dist/metadata/meaning-evidence.d.ts.map +1 -1
- package/dist/metadata/meaning-evidence.js +78 -1
- package/dist/metadata/meaning-evidence.js.map +1 -1
- package/dist/plan-execution-adapter.d.ts +60 -0
- package/dist/plan-execution-adapter.d.ts.map +1 -0
- package/dist/plan-execution-adapter.js +239 -0
- package/dist/plan-execution-adapter.js.map +1 -0
- package/dist/research-governance.d.ts +56 -0
- package/dist/research-governance.d.ts.map +1 -0
- package/dist/research-governance.js +68 -0
- package/dist/research-governance.js.map +1 -0
- package/dist/research-loop.d.ts +14 -0
- package/dist/research-loop.d.ts.map +1 -1
- package/dist/research-loop.js +77 -23
- package/dist/research-loop.js.map +1 -1
- package/dist/resolved-analytical-plan.d.ts +112 -0
- package/dist/resolved-analytical-plan.d.ts.map +1 -0
- package/dist/resolved-analytical-plan.js +346 -0
- package/dist/resolved-analytical-plan.js.map +1 -0
- package/dist/router.d.ts +3 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +34 -10
- package/dist/router.js.map +1 -1
- package/dist/semantic-bridge/member-select.d.ts +6 -0
- package/dist/semantic-bridge/member-select.d.ts.map +1 -1
- package/dist/semantic-bridge/member-select.js +4 -0
- package/dist/semantic-bridge/member-select.js.map +1 -1
- package/dist/semantic-bridge/query-shape.d.ts +26 -0
- package/dist/semantic-bridge/query-shape.d.ts.map +1 -0
- package/dist/semantic-bridge/query-shape.js +48 -0
- package/dist/semantic-bridge/query-shape.js.map +1 -0
- package/package.json +4 -4
package/dist/answer-loop.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { describeDialectForPrompt, } from '@duckcodeailabs/dql-core';
|
|
18
18
|
import { buildSkillBlockHints, buildSkillMetricHints, buildSkillsPrompt, expandQuestionWithSkillVocabulary, selectRelevantSkills } from './skills/loader.js';
|
|
19
19
|
import { detectResultSetOperation, computeResultSetOperation } from './conversation/result-ops.js';
|
|
20
|
+
import { classifyGovernedQueryShape } from './semantic-bridge/query-shape.js';
|
|
20
21
|
import { domainContextSearchDomains } from './domain-context.js';
|
|
21
22
|
import { deriveGeneratedDraftSlug, renderGeneratedSqlDqlArtifact } from './metadata/drafts.js';
|
|
22
23
|
import { buildAnalysisQuestionPlan } from './metadata/analysis-planner.js';
|
|
@@ -40,6 +41,9 @@ import { questionTypeFromText } from './meaning-resolution.js';
|
|
|
40
41
|
import { cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, } from './cascade/cascade.js';
|
|
41
42
|
import { shouldClarifyBeforeGeneration } from './cascade/triage.js';
|
|
42
43
|
import { stampTrustLabel } from './trust/stamp.js';
|
|
44
|
+
import { deriveResolvedAnalyticalPlan } from './resolved-analytical-plan.js';
|
|
45
|
+
import { adaptResolvedAnalyticalPlan, buildPlanExecutionRegistry, } from './plan-execution-adapter.js';
|
|
46
|
+
import { buildGovernedRelationalRegistry, compileGovernedRelationalPlan, finalizeGovernedCompilationReceipt, renderGovernedRelationalDqlArtifact, } from './governed-relational-compiler.js';
|
|
43
47
|
import { QUICK_PROMPT_CONTEXT_BUDGET, canUseLaneRepair, cascadeBudgetTrace, createCascadeBudgetState, deepAlternativeCountForQuestion, promptContextBudgetForQuestion, proposalToolBudgetForQuestion, recordLaneRepair, } from './cascade/budgets.js';
|
|
44
48
|
/**
|
|
45
49
|
* Physical column names whose sampled runtime values include `value`. Used by the
|
|
@@ -459,13 +463,126 @@ function tryCrossResultAnswer(input) {
|
|
|
459
463
|
}
|
|
460
464
|
return answer;
|
|
461
465
|
}
|
|
466
|
+
/**
|
|
467
|
+
* Materialize exactly the Skill IDs/hashes selected in the immutable context
|
|
468
|
+
* pack. No downstream answer route may rerun trigger/domain selection or add a
|
|
469
|
+
* Skill that was not recorded in the KnowledgeLens.
|
|
470
|
+
*
|
|
471
|
+
* Acceptance: SKILL-003, AGT-013.
|
|
472
|
+
*/
|
|
473
|
+
export function materializeKnowledgeLensSkills(contextPack, available) {
|
|
474
|
+
const byIdentity = new Map();
|
|
475
|
+
for (const skill of available) {
|
|
476
|
+
byIdentity.set(skill.qualifiedId ?? skill.id, skill);
|
|
477
|
+
if (!byIdentity.has(skill.id))
|
|
478
|
+
byIdentity.set(skill.id, skill);
|
|
479
|
+
}
|
|
480
|
+
return (contextPack.skills ?? []).map((selected) => {
|
|
481
|
+
const identity = selected.qualifiedId ?? selected.id;
|
|
482
|
+
const source = byIdentity.get(identity) ?? byIdentity.get(selected.id);
|
|
483
|
+
if (source) {
|
|
484
|
+
// Guidance is the immutable, bounded snapshot body. Retain structured
|
|
485
|
+
// source fields but never reread or inject a newer disk body mid-run.
|
|
486
|
+
return { ...source, body: selected.guidance };
|
|
487
|
+
}
|
|
488
|
+
return {
|
|
489
|
+
id: selected.id,
|
|
490
|
+
localId: selected.id,
|
|
491
|
+
qualifiedId: selected.qualifiedId,
|
|
492
|
+
scope: 'project',
|
|
493
|
+
domain: selected.domain,
|
|
494
|
+
domains: selected.domains,
|
|
495
|
+
modelAreaRefs: selected.modelAreaRefs,
|
|
496
|
+
kind: selected.kind,
|
|
497
|
+
status: selected.status,
|
|
498
|
+
owner: selected.owner,
|
|
499
|
+
triggers: selected.triggers,
|
|
500
|
+
exclusions: selected.exclusions,
|
|
501
|
+
description: selected.description,
|
|
502
|
+
preferredMetrics: selected.preferredMetrics,
|
|
503
|
+
preferredBlocks: selected.preferredBlocks,
|
|
504
|
+
preferredDimensions: selected.preferredDimensions,
|
|
505
|
+
requiredFilters: selected.requiredFilters,
|
|
506
|
+
clarifyWhen: selected.clarifyWhen,
|
|
507
|
+
examples: [],
|
|
508
|
+
sourceRefs: selected.sourceRefs,
|
|
509
|
+
vocabulary: selected.vocabulary,
|
|
510
|
+
body: selected.guidance,
|
|
511
|
+
sourcePath: selected.sourcePath ?? `snapshot:${selected.objectKey}`,
|
|
512
|
+
};
|
|
513
|
+
});
|
|
514
|
+
}
|
|
462
515
|
export async function answer(input) {
|
|
516
|
+
const inheritedPlan = !input.resolvedAnalyticalPlan
|
|
517
|
+
&& input.followUp?.priorResolvedAnalyticalPlan
|
|
518
|
+
&& input.followUp.resolvedAnalyticalPlanDelta
|
|
519
|
+
? deriveResolvedAnalyticalPlan(input.followUp.priorResolvedAnalyticalPlan, input.followUp.resolvedAnalyticalPlanDelta)
|
|
520
|
+
: undefined;
|
|
521
|
+
const normalizedInput = inheritedPlan
|
|
522
|
+
? { ...input, resolvedAnalyticalPlan: inheritedPlan }
|
|
523
|
+
: input;
|
|
524
|
+
const resolvedPlanExecutionBinding = normalizedInput.resolvedAnalyticalPlan
|
|
525
|
+
? adaptResolvedAnalyticalPlan({
|
|
526
|
+
plan: normalizedInput.resolvedAnalyticalPlan,
|
|
527
|
+
registry: buildPlanExecutionRegistry({
|
|
528
|
+
nodes: [
|
|
529
|
+
...normalizedInput.kg.getNodesByKind('block', 100_000),
|
|
530
|
+
...normalizedInput.kg.getNodesByKind('metric', 100_000),
|
|
531
|
+
...normalizedInput.kg.getNodesByKind('dimension', 100_000),
|
|
532
|
+
],
|
|
533
|
+
objects: normalizedInput.contextPack?.objects,
|
|
534
|
+
}),
|
|
535
|
+
semanticLayer: normalizedInput.semanticLayer,
|
|
536
|
+
expectedSnapshotId: normalizedInput.contextPack?.knowledgeLens.snapshotId,
|
|
537
|
+
})
|
|
538
|
+
: undefined;
|
|
539
|
+
const planBoundInput = resolvedPlanExecutionBinding
|
|
540
|
+
? { ...normalizedInput, resolvedPlanExecutionBinding }
|
|
541
|
+
: normalizedInput;
|
|
542
|
+
const relationalSchemaContext = planBoundInput.schemaContext?.length
|
|
543
|
+
? planBoundInput.schemaContext
|
|
544
|
+
: (planBoundInput.contextPack?.allowedSqlContext.relations ?? []).map((relation) => ({
|
|
545
|
+
relation: relation.relation,
|
|
546
|
+
name: relation.name,
|
|
547
|
+
columns: relation.columns.map((column) => ({
|
|
548
|
+
name: column.name,
|
|
549
|
+
type: column.type,
|
|
550
|
+
description: column.description,
|
|
551
|
+
})),
|
|
552
|
+
}));
|
|
553
|
+
const governedRelationalCompilation = planBoundInput.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
554
|
+
&& planBoundInput.resolvedAnalyticalPlan.capability === 'governed_relational'
|
|
555
|
+
? compileGovernedRelationalPlan({
|
|
556
|
+
plan: planBoundInput.resolvedAnalyticalPlan,
|
|
557
|
+
registry: buildGovernedRelationalRegistry({
|
|
558
|
+
snapshotId: planBoundInput.resolvedAnalyticalPlan.snapshotId,
|
|
559
|
+
schemaContext: relationalSchemaContext,
|
|
560
|
+
manifest: planBoundInput.manifest,
|
|
561
|
+
}),
|
|
562
|
+
driver: planBoundInput.semanticDriver,
|
|
563
|
+
})
|
|
564
|
+
: undefined;
|
|
565
|
+
const compiledInput = governedRelationalCompilation
|
|
566
|
+
? { ...planBoundInput, governedRelationalCompilation }
|
|
567
|
+
: planBoundInput;
|
|
568
|
+
const executionInput = compiledInput.contextPack
|
|
569
|
+
? {
|
|
570
|
+
...compiledInput,
|
|
571
|
+
skills: materializeKnowledgeLensSkills(compiledInput.contextPack, compiledInput.skills ?? []),
|
|
572
|
+
skillsSelectionLocked: true,
|
|
573
|
+
}
|
|
574
|
+
: compiledInput;
|
|
463
575
|
// Cross-result follow-up ("of these, the average") — computed from the prior
|
|
464
576
|
// rows, before the cascade, so it never re-queries or times out.
|
|
465
|
-
const crossResult = tryCrossResultAnswer(
|
|
466
|
-
if (crossResult)
|
|
467
|
-
return
|
|
468
|
-
|
|
577
|
+
const crossResult = tryCrossResultAnswer(executionInput);
|
|
578
|
+
if (crossResult) {
|
|
579
|
+
return {
|
|
580
|
+
...crossResult,
|
|
581
|
+
resolvedAnalyticalPlan: executionInput.resolvedAnalyticalPlan,
|
|
582
|
+
executablePlan: executionInput.resolvedPlanExecutionBinding,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
const result = applyHollowAnswerGate(await runAnswerLoop(executionInput));
|
|
469
586
|
// Attach the canonical trust label once, at the single exit point, so every
|
|
470
587
|
// return site inside runAnswerLoop stays untouched and backward compatible.
|
|
471
588
|
// Freshness-aware trust: for a certified answer, fold the source block's data
|
|
@@ -494,6 +611,8 @@ export async function answer(input) {
|
|
|
494
611
|
...publicResult,
|
|
495
612
|
domainContext: input.domainContext,
|
|
496
613
|
intentDecision,
|
|
614
|
+
resolvedAnalyticalPlan: executionInput.resolvedAnalyticalPlan,
|
|
615
|
+
executablePlan: executionInput.resolvedPlanExecutionBinding,
|
|
497
616
|
trustLabelInfo,
|
|
498
617
|
provenanceFooter: buildProvenanceFooter(result, trustLabelInfo),
|
|
499
618
|
cascade: publicResult.cascade ?? createCascadeAnswerResult({
|
|
@@ -513,15 +632,17 @@ export async function answer(input) {
|
|
|
513
632
|
// Stamp the SELECTED skills that shaped the answer (transparency). Computed
|
|
514
633
|
// here so every return site inside runAnswerLoop stays untouched.
|
|
515
634
|
appliedSkills: result.appliedSkills ??
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
635
|
+
(executionInput.skillsSelectionLocked
|
|
636
|
+
? executionInput.skills ?? []
|
|
637
|
+
: selectRelevantSkills(executionInput.skills ?? [], executionInput.question, {
|
|
638
|
+
userId: executionInput.userId ?? null,
|
|
639
|
+
modelAreaIds: executionInput.domainContext?.modelAreaId ? [executionInput.domainContext.modelAreaId] : [],
|
|
640
|
+
domains: Array.from(new Set([
|
|
641
|
+
...domainContextSearchDomains(executionInput.domainContext),
|
|
642
|
+
...(executionInput.domain ? [executionInput.domain] : []),
|
|
643
|
+
...(executionInput.contextPack?.objects ?? []).slice(0, 20).flatMap((object) => object.domain ? [object.domain] : []),
|
|
644
|
+
])),
|
|
645
|
+
})).map((s) => ({
|
|
525
646
|
id: s.id,
|
|
526
647
|
description: s.description,
|
|
527
648
|
})),
|
|
@@ -613,11 +734,13 @@ async function runAnswerLoop(input) {
|
|
|
613
734
|
? (input.contextPack?.objects ?? []).slice(0, 20).flatMap((object) => object.domain ? [object.domain] : [])
|
|
614
735
|
: []),
|
|
615
736
|
]));
|
|
616
|
-
const selectedSkills =
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
737
|
+
const selectedSkills = input.skillsSelectionLocked
|
|
738
|
+
? skills
|
|
739
|
+
: selectRelevantSkills(skills, question, {
|
|
740
|
+
userId: userId ?? null,
|
|
741
|
+
domains: inferredDomains,
|
|
742
|
+
modelAreaIds: input.domainContext?.modelAreaId ? [input.domainContext.modelAreaId] : [],
|
|
743
|
+
});
|
|
621
744
|
const effectiveBlockHints = Array.from(new Set([
|
|
622
745
|
...blockHints,
|
|
623
746
|
// Only selected skills may influence block ranking. Previously a preferred
|
|
@@ -653,6 +776,85 @@ async function runAnswerLoop(input) {
|
|
|
653
776
|
// columns—not copy a customer-grain worked example into a product-grain ask.
|
|
654
777
|
const promptContextPack = contextPackForRequestedShape(scopedContextPack, question, questionPlan, kg);
|
|
655
778
|
const repairBudgetState = createCascadeBudgetState(input.cascadeBudgetModel);
|
|
779
|
+
const authoritativePlanBinding = input.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
780
|
+
? input.resolvedPlanExecutionBinding
|
|
781
|
+
: undefined;
|
|
782
|
+
const governedRelationalCompilation = input.governedRelationalCompilation;
|
|
783
|
+
if (governedRelationalCompilation?.status === 'blocked') {
|
|
784
|
+
const text = `The governed relational plan cannot compile safely: ${governedRelationalCompilation.reason}`;
|
|
785
|
+
return {
|
|
786
|
+
kind: 'no_answer',
|
|
787
|
+
sourceTier: 'no_answer',
|
|
788
|
+
certification: 'analyst_review_required',
|
|
789
|
+
reviewStatus: 'none',
|
|
790
|
+
confidence: 0,
|
|
791
|
+
text,
|
|
792
|
+
answer: text,
|
|
793
|
+
refusalCode: governedRelationalCompilation.code.startsWith('RELATIONSHIP_') ? 'modeling_gap' : 'grounding_gap',
|
|
794
|
+
refusalDetails: { code: 'grounding_gap', message: `${governedRelationalCompilation.code}: ${governedRelationalCompilation.reason}` },
|
|
795
|
+
citations: contextPackCitations(input.contextPack, 8),
|
|
796
|
+
considered,
|
|
797
|
+
contextPack: input.contextPack,
|
|
798
|
+
providerUsed: provider.name,
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
if (governedRelationalCompilation?.status === 'compiled') {
|
|
802
|
+
const dqlArtifact = renderGovernedRelationalDqlArtifact(governedRelationalCompilation);
|
|
803
|
+
let result;
|
|
804
|
+
let executionError;
|
|
805
|
+
let receipt = governedRelationalCompilation.receipt;
|
|
806
|
+
if (input.executeDqlArtifact) {
|
|
807
|
+
try {
|
|
808
|
+
result = await input.executeDqlArtifact(dqlArtifact);
|
|
809
|
+
receipt = finalizeGovernedCompilationReceipt(receipt, result);
|
|
810
|
+
}
|
|
811
|
+
catch (error) {
|
|
812
|
+
executionError = error instanceof Error ? error.message : String(error);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
const text = executionError
|
|
816
|
+
? `The governed relational query compiled, but execution failed: ${executionError}`
|
|
817
|
+
: result
|
|
818
|
+
? `Compiled and executed the snapshot-bound governed relational plan. Returned ${result.rowCount} row${result.rowCount === 1 ? '' : 's'}.`
|
|
819
|
+
: 'Compiled the snapshot-bound governed relational plan. Execution was not requested.';
|
|
820
|
+
return {
|
|
821
|
+
kind: executionError ? 'no_answer' : 'uncertified',
|
|
822
|
+
sourceTier: executionError ? 'no_answer' : 'dbt_manifest',
|
|
823
|
+
certification: executionError ? 'analyst_review_required' : 'governed',
|
|
824
|
+
reviewStatus: executionError ? 'analyst_review_required' : 'governed',
|
|
825
|
+
confidence: executionError ? 0 : 0.9,
|
|
826
|
+
text,
|
|
827
|
+
answer: text,
|
|
828
|
+
...(executionError ? { executionError, refusalCode: 'grounding_gap' } : {}),
|
|
829
|
+
proposedSql: governedRelationalCompilation.sql,
|
|
830
|
+
sql: governedRelationalCompilation.sql,
|
|
831
|
+
dqlArtifact,
|
|
832
|
+
result,
|
|
833
|
+
governedCompilationReceipt: receipt,
|
|
834
|
+
citations: schemaCitations(schemaContext, 8),
|
|
835
|
+
considered,
|
|
836
|
+
contextPack: input.contextPack,
|
|
837
|
+
providerUsed: provider.name,
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
if (authoritativePlanBinding?.status === 'blocked') {
|
|
841
|
+
const text = `The resolved analytical plan cannot execute safely: ${authoritativePlanBinding.reason}`;
|
|
842
|
+
return {
|
|
843
|
+
kind: 'no_answer',
|
|
844
|
+
sourceTier: 'no_answer',
|
|
845
|
+
certification: 'analyst_review_required',
|
|
846
|
+
reviewStatus: 'none',
|
|
847
|
+
confidence: 0,
|
|
848
|
+
text,
|
|
849
|
+
answer: text,
|
|
850
|
+
refusalCode: authoritativePlanBinding.code === 'PLAN_BLOCKED' ? 'ambiguous' : 'grounding_gap',
|
|
851
|
+
refusalDetails: { code: authoritativePlanBinding.code, message: authoritativePlanBinding.reason },
|
|
852
|
+
citations: contextPackCitations(input.contextPack, 8),
|
|
853
|
+
considered,
|
|
854
|
+
contextPack: input.contextPack,
|
|
855
|
+
providerUsed: provider.name,
|
|
856
|
+
};
|
|
857
|
+
}
|
|
656
858
|
const fallbackIntent = classifyAgentIntent({
|
|
657
859
|
question,
|
|
658
860
|
followUp: input.followUp,
|
|
@@ -672,16 +874,25 @@ async function runAnswerLoop(input) {
|
|
|
672
874
|
if (node && !semanticMetricNodes.some((candidate) => candidate.nodeId === node.nodeId))
|
|
673
875
|
semanticMetricNodes.push(node);
|
|
674
876
|
}
|
|
675
|
-
const
|
|
877
|
+
const authoritativeSemanticBinding = authoritativePlanBinding?.status === 'ready'
|
|
878
|
+
&& authoritativePlanBinding.kind === 'semantic'
|
|
879
|
+
? authoritativePlanBinding
|
|
880
|
+
: undefined;
|
|
881
|
+
const preferredSemanticMetric = authoritativeSemanticBinding?.metricNode
|
|
882
|
+
?? (input.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
883
|
+
? undefined
|
|
884
|
+
: resolvePreferredSemanticMetric([input.preferredExecutionId, ...(input.preferredEvidenceIds ?? [])], semanticMetricNodes, kg));
|
|
676
885
|
const semanticLayerForExec = input.semanticLayer;
|
|
677
886
|
const canExecuteSemanticMetricForMatch = input.canExecuteSemanticMetric
|
|
678
887
|
?? (semanticLayerForExec ? (name) => semanticLayerForExec.canComposeMetric(name) : undefined);
|
|
679
888
|
let semanticMetricMatch = preferredSemanticMetric
|
|
680
889
|
? { metric: preferredSemanticMetric, score: 1, basis: 'name' }
|
|
681
|
-
:
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
890
|
+
: input.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
891
|
+
? null
|
|
892
|
+
: await matchSemanticMetric(semanticQuestion, semanticMetricNodes, {
|
|
893
|
+
measureTerms: [...questionPlan.requestedShape.measures, ...questionPlan.metricTerms],
|
|
894
|
+
...(canExecuteSemanticMetricForMatch ? { canExecute: canExecuteSemanticMetricForMatch } : {}),
|
|
895
|
+
}).catch(() => null);
|
|
685
896
|
// Stage 1: certified artifact match. Blocks can be executed; dashboards,
|
|
686
897
|
// Apps, and notebooks are returned as governed citations/navigation targets.
|
|
687
898
|
const drilldownCertifiedHit = input.followUp?.kind === 'drilldown'
|
|
@@ -725,8 +936,16 @@ async function runAnswerLoop(input) {
|
|
|
725
936
|
excludedArtifactIds,
|
|
726
937
|
kg,
|
|
727
938
|
}) : null;
|
|
728
|
-
|
|
729
|
-
|
|
939
|
+
const authoritativeCertifiedBinding = authoritativePlanBinding?.status === 'ready'
|
|
940
|
+
&& authoritativePlanBinding.kind === 'certified'
|
|
941
|
+
? authoritativePlanBinding
|
|
942
|
+
: undefined;
|
|
943
|
+
let artifactHit = authoritativeCertifiedBinding
|
|
944
|
+
? { node: authoritativeCertifiedBinding.node, score: 1 }
|
|
945
|
+
: input.resolvedAnalyticalPlan?.mode === 'authoritative'
|
|
946
|
+
? null
|
|
947
|
+
: drilldownCertifiedHit ?? unsafeCatalogCertifiedHit
|
|
948
|
+
?? (catalogCertifiedHit ? null : fallbackCertifiedHit);
|
|
730
949
|
let certifiedExecutionFallback;
|
|
731
950
|
// Certified remains first when it actually covers the question. If the
|
|
732
951
|
// retrieved block does not fit but a governed semantic metric does, never
|
|
@@ -874,8 +1093,10 @@ async function runAnswerLoop(input) {
|
|
|
874
1093
|
const dqlArtifact = buildCertifiedBlockDqlArtifact(artifactHit.node, result, questionPlan.requestedShape.topN?.scope === 'per_group'
|
|
875
1094
|
? undefined
|
|
876
1095
|
: questionPlan.requestedShape.topN?.n);
|
|
1096
|
+
const authoritativeCertifiedFailure = Boolean(authoritativeCertifiedBinding && executionError);
|
|
877
1097
|
const recoverableCertifiedFailure = artifactHit.node.kind === 'block'
|
|
878
1098
|
&& executionError !== undefined
|
|
1099
|
+
&& !authoritativeCertifiedFailure
|
|
879
1100
|
&& isRetryableCertifiedExecutionError(executionError);
|
|
880
1101
|
if (recoverableCertifiedFailure) {
|
|
881
1102
|
// A certified artifact is trusted evidence, not an obligation to return a
|
|
@@ -888,16 +1109,17 @@ async function runAnswerLoop(input) {
|
|
|
888
1109
|
}
|
|
889
1110
|
else {
|
|
890
1111
|
return {
|
|
891
|
-
kind: certifiedShapePassed ? 'certified' : 'uncertified',
|
|
892
|
-
sourceTier,
|
|
1112
|
+
kind: authoritativeCertifiedFailure ? 'no_answer' : certifiedShapePassed ? 'certified' : 'uncertified',
|
|
1113
|
+
sourceTier: authoritativeCertifiedFailure ? 'no_answer' : sourceTier,
|
|
893
1114
|
certification: certifiedShapePassed ? 'certified' : 'analyst_review_required',
|
|
894
|
-
reviewStatus: certifiedShapePassed ? 'certified' : 'analyst_review_required',
|
|
1115
|
+
reviewStatus: authoritativeCertifiedFailure ? 'none' : certifiedShapePassed ? 'certified' : 'analyst_review_required',
|
|
895
1116
|
confidence: certifiedShapePassed ? 0.95 : 0.45,
|
|
896
1117
|
text,
|
|
897
1118
|
answer: text,
|
|
898
1119
|
block: artifactHit.node.kind === 'block' ? artifactHit.node : undefined,
|
|
899
1120
|
result,
|
|
900
1121
|
executionError,
|
|
1122
|
+
...(authoritativeCertifiedFailure ? { refusalCode: 'grounding_gap' } : {}),
|
|
901
1123
|
sql: result?.sql,
|
|
902
1124
|
dqlArtifact,
|
|
903
1125
|
trustLabel: certifiedShapePassed ? input.contextPack?.trustLabel ?? 'certified' : 'mixed',
|
|
@@ -1094,7 +1316,44 @@ async function runAnswerLoop(input) {
|
|
|
1094
1316
|
let semanticBridgeAnswer;
|
|
1095
1317
|
let semanticRuntimeFailure;
|
|
1096
1318
|
let semanticRuntimeCompiledAnswer = false;
|
|
1097
|
-
if (input.semanticLayer
|
|
1319
|
+
if (authoritativeSemanticBinding && input.semanticLayer) {
|
|
1320
|
+
const selection = authoritativeSemanticBinding.selection;
|
|
1321
|
+
semanticBridgeAnswer = composeSemanticQueryFromMembers({
|
|
1322
|
+
semanticLayer: input.semanticLayer,
|
|
1323
|
+
question,
|
|
1324
|
+
selection,
|
|
1325
|
+
...(input.semanticDriver ? { driver: input.semanticDriver } : {}),
|
|
1326
|
+
...(input.semanticTableMapping ? { tableMapping: input.semanticTableMapping } : {}),
|
|
1327
|
+
});
|
|
1328
|
+
if (!semanticBridgeAnswer && input.semanticQueryCompiler) {
|
|
1329
|
+
try {
|
|
1330
|
+
const compiled = await input.semanticQueryCompiler(selection);
|
|
1331
|
+
semanticBridgeAnswer = composeSemanticQueryFromCompiledMembers({
|
|
1332
|
+
semanticLayer: input.semanticLayer,
|
|
1333
|
+
question,
|
|
1334
|
+
selection: compiled.selection ?? selection,
|
|
1335
|
+
sql: compiled.sql,
|
|
1336
|
+
});
|
|
1337
|
+
semanticRuntimeCompiledAnswer = Boolean(semanticBridgeAnswer && compiled.engine !== 'native');
|
|
1338
|
+
}
|
|
1339
|
+
catch (error) {
|
|
1340
|
+
semanticRuntimeFailure = error instanceof Error ? error.message : String(error);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
if (semanticBridgeAnswer) {
|
|
1344
|
+
semanticBridgeToolCalls.push({
|
|
1345
|
+
name: 'compile_resolved_analytical_plan',
|
|
1346
|
+
status: 'checked',
|
|
1347
|
+
inputSummary: `plan: ${authoritativeSemanticBinding.planId}; metric: ${selection.metrics.join(', ')}`,
|
|
1348
|
+
outputSummary: 'Compiled the exact snapshot-bound member selection without rematching.',
|
|
1349
|
+
order: 1,
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
else if (!semanticRuntimeFailure) {
|
|
1353
|
+
semanticRuntimeFailure = `The exact plan ${authoritativeSemanticBinding.planId} is not composable by the pinned semantic runtime.`;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
if (!authoritativeSemanticBinding && input.semanticLayer && semanticMetricMatch) {
|
|
1098
1357
|
semanticBridgeAnswer = composeSemanticQueryForQuestion({
|
|
1099
1358
|
semanticLayer: input.semanticLayer,
|
|
1100
1359
|
question,
|
|
@@ -1152,16 +1411,19 @@ async function runAnswerLoop(input) {
|
|
|
1152
1411
|
// spend another model call re-selecting the same member or silently fall
|
|
1153
1412
|
// into exploratory SQL; surface the compiler's actionable error instead.
|
|
1154
1413
|
if (!semanticBridgeAnswer && !semanticRuntimeFailure) {
|
|
1155
|
-
const
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1414
|
+
const requestedGroupingDims = requestedGroupingDimensions(questionPlan);
|
|
1415
|
+
const wantedBreakdown = questionPlan.requestedShape.dimensions.length > 0
|
|
1416
|
+
|| questionPlan.dimensionTerms.length > 0;
|
|
1417
|
+
const requiredGroupingCount = requestedGroupingDims.length;
|
|
1418
|
+
// Capture the narrowed semantic layer so the async closure below keeps the
|
|
1419
|
+
// non-undefined type (TS resets narrowing across closure boundaries).
|
|
1420
|
+
const bridgeLayer = input.semanticLayer;
|
|
1421
|
+
// Compose a member selection: native first, then the runtime compiler for
|
|
1422
|
+
// metrics native can't express. Side effects (runtime flags, tool calls)
|
|
1423
|
+
// are recorded here so a retry reruns them cleanly.
|
|
1424
|
+
const composeSelection = async (selection) => {
|
|
1163
1425
|
let composed = composeSemanticQueryFromMembers({
|
|
1164
|
-
semanticLayer:
|
|
1426
|
+
semanticLayer: bridgeLayer,
|
|
1165
1427
|
question,
|
|
1166
1428
|
selection,
|
|
1167
1429
|
...(input.semanticDriver ? { driver: input.semanticDriver } : {}),
|
|
@@ -1171,7 +1433,7 @@ async function runAnswerLoop(input) {
|
|
|
1171
1433
|
try {
|
|
1172
1434
|
const compiled = await input.semanticQueryCompiler(selection);
|
|
1173
1435
|
composed = composeSemanticQueryFromCompiledMembers({
|
|
1174
|
-
semanticLayer:
|
|
1436
|
+
semanticLayer: bridgeLayer,
|
|
1175
1437
|
question,
|
|
1176
1438
|
selection: compiled.selection ?? selection,
|
|
1177
1439
|
sql: compiled.sql,
|
|
@@ -1191,18 +1453,44 @@ async function runAnswerLoop(input) {
|
|
|
1191
1453
|
semanticRuntimeFailure = error instanceof Error ? error.message : String(error);
|
|
1192
1454
|
}
|
|
1193
1455
|
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1456
|
+
return composed;
|
|
1457
|
+
};
|
|
1458
|
+
// A governed answer that silently DROPS the requested grouping is governed-
|
|
1459
|
+
// but-wrong. Detect it so we retry (forcing the breakdown) rather than
|
|
1460
|
+
// adopting a scalar answer to a breakdown question.
|
|
1461
|
+
const dropsBreakdown = (composed) => Boolean(composed) && wantedBreakdown
|
|
1462
|
+
&& composed.dimensions.length < Math.max(1, requiredGroupingCount)
|
|
1463
|
+
&& !composed.timeDimension;
|
|
1464
|
+
let selection = await selectSemanticMembersViaLlm({
|
|
1465
|
+
provider,
|
|
1466
|
+
semanticLayer: input.semanticLayer,
|
|
1467
|
+
question,
|
|
1468
|
+
signal: input.signal,
|
|
1469
|
+
reasoningEffort: input.reasoningEffort,
|
|
1470
|
+
});
|
|
1471
|
+
if (selection) {
|
|
1472
|
+
let composed = await composeSelection(selection);
|
|
1473
|
+
// Retry ONCE forcing the requested breakdown dimensions before giving up
|
|
1474
|
+
// on the governed lane — the model sometimes omits a group-by it could
|
|
1475
|
+
// have kept. If it still can't, the Tier-2.5 gate decides refuse-vs-SQL.
|
|
1476
|
+
if (composed && dropsBreakdown(composed) && requestedGroupingDims.length > 0 && !semanticRuntimeFailure) {
|
|
1477
|
+
const retry = await selectSemanticMembersViaLlm({
|
|
1478
|
+
provider,
|
|
1479
|
+
semanticLayer: input.semanticLayer,
|
|
1480
|
+
question,
|
|
1481
|
+
signal: input.signal,
|
|
1482
|
+
reasoningEffort: input.reasoningEffort,
|
|
1483
|
+
requireDimensions: requestedGroupingDims,
|
|
1484
|
+
});
|
|
1485
|
+
if (retry) {
|
|
1486
|
+
const retryComposed = await composeSelection(retry);
|
|
1487
|
+
if (retryComposed && !dropsBreakdown(retryComposed)) {
|
|
1488
|
+
composed = retryComposed;
|
|
1489
|
+
selection = retry;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
if (composed && !dropsBreakdown(composed)) {
|
|
1206
1494
|
semanticBridgeAnswer = composed;
|
|
1207
1495
|
semanticBridgeToolCalls.push({
|
|
1208
1496
|
name: 'query_semantic_model',
|
|
@@ -1345,6 +1633,40 @@ async function runAnswerLoop(input) {
|
|
|
1345
1633
|
// measure and only generates the join/grouping around it. Keeps the number
|
|
1346
1634
|
// consistent with the governed metric. Degrades to plain generation when the metric
|
|
1347
1635
|
// has no resolvable definition. The grain ledger + validation still gate the output.
|
|
1636
|
+
// Structured-selection gate: a governed metric matched but the compiler-owned
|
|
1637
|
+
// lanes above all failed. Refuse-with-reason ONLY when we are CONFIDENT the
|
|
1638
|
+
// failure is a MODELING gap — the requested breakdown resolves to a DECLARED
|
|
1639
|
+
// dimension the semantic models can't join to this metric (no_join_path). In
|
|
1640
|
+
// that one case free SQL would misjoin or reference an ambiguous column, so we
|
|
1641
|
+
// list the connected dimensions instead of guessing. Everything else — a
|
|
1642
|
+
// paraphrase the compile tool can still resolve, a raw column answerable by
|
|
1643
|
+
// safe single-table SQL, a genuine compiler gap (per-group topN/arithmetic/
|
|
1644
|
+
// window/HAVING), or a derived metric needing the runtime — keeps its existing
|
|
1645
|
+
// path (the compile_semantic_query tool + metric-anchored generation below).
|
|
1646
|
+
if (input.semanticLayer && semanticMetricMatch) {
|
|
1647
|
+
const shape = classifyGovernedQueryShape(questionPlan, semanticMetricMatch.metric.name, input.semanticLayer);
|
|
1648
|
+
const modelingGap = shape === 'genuine_gap'
|
|
1649
|
+
? undefined
|
|
1650
|
+
: resolveModelingGap(questionPlan, semanticMetricMatch.metric.name, input.semanticLayer);
|
|
1651
|
+
if (modelingGap) {
|
|
1652
|
+
return buildModelingGapRefusal({
|
|
1653
|
+
question,
|
|
1654
|
+
metricName: modelingGap.metricName,
|
|
1655
|
+
incompatibleDimension: modelingGap.incompatibleDim,
|
|
1656
|
+
semanticLayer: input.semanticLayer,
|
|
1657
|
+
questionPlan,
|
|
1658
|
+
contextPack: input.contextPack,
|
|
1659
|
+
considered,
|
|
1660
|
+
memoryContext: input.memoryContext,
|
|
1661
|
+
artifactHits,
|
|
1662
|
+
businessHits,
|
|
1663
|
+
semanticHits,
|
|
1664
|
+
manifestHits,
|
|
1665
|
+
toolCalls: semanticBridgeToolCalls,
|
|
1666
|
+
providerName: provider.name,
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1348
1670
|
const metricAnchor = semanticMetricMatch && input.semanticLayer
|
|
1349
1671
|
? resolveGovernedMetricDefinition(semanticMetricMatch.metric, semanticMetricNodes, input.semanticLayer)
|
|
1350
1672
|
: undefined;
|
|
@@ -2082,7 +2404,7 @@ async function runAnswerLoop(input) {
|
|
|
2082
2404
|
catch (err) {
|
|
2083
2405
|
executionError = err instanceof Error ? err.message : String(err);
|
|
2084
2406
|
}
|
|
2085
|
-
if (executionError && !fanoutContradiction) {
|
|
2407
|
+
if (executionError && !fanoutContradiction && !authoritativeSemanticBinding) {
|
|
2086
2408
|
if (isRetryableGeneratedSqlError(executionError)) {
|
|
2087
2409
|
const localRepairSql = repairGeneratedSqlLocally(parsed.sql, executionError, schemaContext);
|
|
2088
2410
|
if (localRepairSql && canUseLaneRepair(repairBudgetState, 'execution')) {
|
|
@@ -2304,19 +2626,21 @@ async function runAnswerLoop(input) {
|
|
|
2304
2626
|
? semanticMetricMatch?.metric.certification
|
|
2305
2627
|
: undefined;
|
|
2306
2628
|
const certifiedMetricAnswer = semanticMetricCertification === 'certified' || semanticMetricCertification === 'reviewed';
|
|
2629
|
+
const governedMetricExecutionFailure = governedMetricAnswer && Boolean(executionError);
|
|
2307
2630
|
return {
|
|
2308
|
-
kind: 'uncertified',
|
|
2309
|
-
sourceTier: governedMetricAnswer ? 'semantic_layer' : activeTier,
|
|
2310
|
-
certification: governedMetricAnswer ? 'governed' : 'ai_generated',
|
|
2311
|
-
reviewStatus: governedMetricAnswer ? 'governed' : 'draft_ready',
|
|
2631
|
+
kind: governedMetricExecutionFailure ? 'no_answer' : 'uncertified',
|
|
2632
|
+
sourceTier: governedMetricExecutionFailure ? 'no_answer' : governedMetricAnswer ? 'semantic_layer' : activeTier,
|
|
2633
|
+
certification: governedMetricExecutionFailure ? 'analyst_review_required' : governedMetricAnswer ? 'governed' : 'ai_generated',
|
|
2634
|
+
reviewStatus: governedMetricExecutionFailure ? 'none' : governedMetricAnswer ? 'governed' : 'draft_ready',
|
|
2312
2635
|
semanticMetricCertification,
|
|
2313
|
-
confidence: certifiedMetricAnswer ? 0.8 : governedMetricAnswer ? 0.72 : 0.55,
|
|
2636
|
+
confidence: governedMetricExecutionFailure ? 0 : certifiedMetricAnswer ? 0.8 : governedMetricAnswer ? 0.72 : 0.55,
|
|
2314
2637
|
text: generatedText,
|
|
2315
2638
|
answer: generatedText,
|
|
2316
2639
|
proposedSql: parsed.sql,
|
|
2317
2640
|
sql: parsed.sql,
|
|
2318
2641
|
result,
|
|
2319
2642
|
executionError,
|
|
2643
|
+
...(governedMetricExecutionFailure ? { refusalCode: 'grounding_gap' } : {}),
|
|
2320
2644
|
suggestedViz: parsed.viz ?? 'table',
|
|
2321
2645
|
dqlArtifact: answerDqlArtifact,
|
|
2322
2646
|
draftBlock,
|
|
@@ -4972,6 +5296,115 @@ function isRetryableCertifiedExecutionError(error) {
|
|
|
4972
5296
|
|| /\bcolumn\b.*\b(?:not\s+found|does\s+not\s+exist|not\s+recognized|unknown)\b/i.test(error)
|
|
4973
5297
|
|| /\btable\b.*\b(?:not\s+found|does\s+not\s+exist|unknown)\b/i.test(error);
|
|
4974
5298
|
}
|
|
5299
|
+
/**
|
|
5300
|
+
* A governed metric matched, the compiler-owned lanes couldn't build the exact
|
|
5301
|
+
* shape, and the shape is one the compiler SHOULD handle — so the gap is a
|
|
5302
|
+
* missing join/model relationship, not a reason to guess SQL. Refuse with the
|
|
5303
|
+
* connected dimensions so the user knows exactly what to add. Mirrors the
|
|
5304
|
+
* AGT-005 semantic-runtime-failure no_answer shape.
|
|
5305
|
+
*/
|
|
5306
|
+
/**
|
|
5307
|
+
* Confident modeling-gap detector: does the requested breakdown resolve to a
|
|
5308
|
+
* DECLARED dimension the semantic models can't join to this metric? Only then is
|
|
5309
|
+
* a compose failure a real modeling gap (list the connected dims, refuse) rather
|
|
5310
|
+
* than a paraphrase the compile tool can resolve or a raw column safe SQL can
|
|
5311
|
+
* answer. Matching is intentionally CONSERVATIVE (name/label/qualified equality),
|
|
5312
|
+
* so an uncertain match falls through to generation — never a false refusal.
|
|
5313
|
+
*/
|
|
5314
|
+
function resolveModelingGap(questionPlan, metricName, semanticLayer) {
|
|
5315
|
+
const canonical = semanticLayer.listMetrics().find((metric) => metric.name === metricName
|
|
5316
|
+
|| metric.name.endsWith(`.${metricName}`)
|
|
5317
|
+
|| metricName.endsWith(`.${metric.name}`))?.name ?? metricName;
|
|
5318
|
+
let explained;
|
|
5319
|
+
try {
|
|
5320
|
+
explained = semanticLayer.explainCompatibleDimensions([canonical]);
|
|
5321
|
+
}
|
|
5322
|
+
catch {
|
|
5323
|
+
return undefined;
|
|
5324
|
+
}
|
|
5325
|
+
const { compatible, incompatible } = explained;
|
|
5326
|
+
// A declared dimension the join graph can't reach from this metric. Ignore
|
|
5327
|
+
// metric_unresolved (a name-resolution miss, not a user-facing modeling gap).
|
|
5328
|
+
const gappy = incompatible.filter((dim) => dim.reason === 'no_join_path' || dim.reason === 'not_shared_across_metrics');
|
|
5329
|
+
if (gappy.length === 0)
|
|
5330
|
+
return undefined;
|
|
5331
|
+
const requested = [
|
|
5332
|
+
...questionPlan.requestedShape.dimensions,
|
|
5333
|
+
...questionPlan.dimensionTerms,
|
|
5334
|
+
].map((value) => value.trim()).filter(Boolean);
|
|
5335
|
+
if (requested.length === 0)
|
|
5336
|
+
return undefined;
|
|
5337
|
+
const lastSegment = (name) => name?.split(/[._]/).pop();
|
|
5338
|
+
const matchesDim = (phrase, dim) => conceptsEquivalent(phrase, dim.name)
|
|
5339
|
+
|| (dim.label ? conceptsEquivalent(phrase, dim.label) : false)
|
|
5340
|
+
|| (dim.qualifiedName ? conceptsEquivalent(phrase, dim.qualifiedName) : false)
|
|
5341
|
+
|| (lastSegment(dim.qualifiedName) ? conceptsEquivalent(phrase, lastSegment(dim.qualifiedName)) : false);
|
|
5342
|
+
for (const phrase of requested) {
|
|
5343
|
+
// A phrase that also matches a CONNECTED dimension is not a gap — the compose/
|
|
5344
|
+
// retry lane owns it. Only refuse when the phrase matches an unconnected dim
|
|
5345
|
+
// and nothing connected.
|
|
5346
|
+
if (compatible.some((dim) => matchesDim(phrase, dim)))
|
|
5347
|
+
continue;
|
|
5348
|
+
const hit = gappy.find((dim) => matchesDim(phrase, dim));
|
|
5349
|
+
if (hit) {
|
|
5350
|
+
const connected = compatible.map((dim) => dim.label || dim.name).slice(0, 12);
|
|
5351
|
+
return { metricName: canonical, incompatibleDim: hit.name, connected };
|
|
5352
|
+
}
|
|
5353
|
+
}
|
|
5354
|
+
return undefined;
|
|
5355
|
+
}
|
|
5356
|
+
function buildModelingGapRefusal(input) {
|
|
5357
|
+
const compatible = (() => {
|
|
5358
|
+
try {
|
|
5359
|
+
return input.semanticLayer.explainCompatibleDimensions([input.metricName]).compatible;
|
|
5360
|
+
}
|
|
5361
|
+
catch {
|
|
5362
|
+
return [];
|
|
5363
|
+
}
|
|
5364
|
+
})();
|
|
5365
|
+
const connected = compatible.map((dimension) => dimension.label || dimension.name).slice(0, 12);
|
|
5366
|
+
const requested = [
|
|
5367
|
+
...input.questionPlan.requestedShape.dimensions,
|
|
5368
|
+
...input.questionPlan.dimensionTerms,
|
|
5369
|
+
].filter((value, index, all) => value && all.indexOf(value) === index);
|
|
5370
|
+
const requestedText = input.incompatibleDimension
|
|
5371
|
+
? `by ${input.incompatibleDimension}`
|
|
5372
|
+
: requested.length > 0 ? `by ${requested.join(', ')}` : 'by the requested breakdown';
|
|
5373
|
+
const text = [
|
|
5374
|
+
`“${input.metricName}” can’t be grouped ${requestedText} — the semantic models don’t declare a join path between them.`,
|
|
5375
|
+
connected.length > 0
|
|
5376
|
+
? `It CAN be grouped by: ${connected.join(', ')}.`
|
|
5377
|
+
: 'No compatible group-by dimension is modeled for this metric.',
|
|
5378
|
+
'To enable this breakdown, add the relationship (a join/entity) in the dbt semantic models and recompile. Nothing was executed.',
|
|
5379
|
+
].join(' ');
|
|
5380
|
+
return {
|
|
5381
|
+
kind: 'no_answer',
|
|
5382
|
+
sourceTier: 'no_answer',
|
|
5383
|
+
certification: 'analyst_review_required',
|
|
5384
|
+
reviewStatus: 'none',
|
|
5385
|
+
confidence: 0,
|
|
5386
|
+
text,
|
|
5387
|
+
answer: text,
|
|
5388
|
+
refusalCode: 'modeling_gap',
|
|
5389
|
+
refusalDetails: { code: 'modeling_gap', message: text },
|
|
5390
|
+
citations: [],
|
|
5391
|
+
memoryContext: input.memoryContext,
|
|
5392
|
+
evidence: buildNoAnswerEvidence({
|
|
5393
|
+
question: input.question,
|
|
5394
|
+
reason: text,
|
|
5395
|
+
artifactHits: input.artifactHits,
|
|
5396
|
+
businessHits: input.businessHits,
|
|
5397
|
+
semanticHits: input.semanticHits,
|
|
5398
|
+
manifestHits: input.manifestHits,
|
|
5399
|
+
considered: input.considered,
|
|
5400
|
+
memoryContext: input.memoryContext ?? [],
|
|
5401
|
+
toolCalls: input.toolCalls,
|
|
5402
|
+
}),
|
|
5403
|
+
contextPack: input.contextPack,
|
|
5404
|
+
considered: input.considered,
|
|
5405
|
+
providerUsed: input.providerName,
|
|
5406
|
+
};
|
|
5407
|
+
}
|
|
4975
5408
|
function repairGeneratedSqlLocally(sql, error, schemaContext) {
|
|
4976
5409
|
// Ambiguous column: a bare column exists in more than one joined table (e.g. a
|
|
4977
5410
|
// conformed `report_as_of_dt` on both the fact and a joined dimension), and the
|