@duckcodeailabs/dql-agent 1.6.21 → 1.6.22
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 +287 -0
- package/dist/agent-run-engine.d.ts.map +1 -0
- package/dist/agent-run-engine.js +916 -0
- package/dist/agent-run-engine.js.map +1 -0
- package/dist/agent-run-gates.d.ts +26 -0
- package/dist/agent-run-gates.d.ts.map +1 -0
- package/dist/agent-run-gates.js +170 -0
- package/dist/agent-run-gates.js.map +1 -0
- package/dist/agent-run-planner.d.ts +32 -0
- package/dist/agent-run-planner.d.ts.map +1 -0
- package/dist/agent-run-planner.js +280 -0
- package/dist/agent-run-planner.js.map +1 -0
- package/dist/answer-loop.d.ts +49 -0
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +292 -17
- package/dist/answer-loop.js.map +1 -1
- package/dist/app-builder.d.ts +6 -0
- package/dist/app-builder.d.ts.map +1 -1
- package/dist/app-builder.js +95 -2
- package/dist/app-builder.js.map +1 -1
- package/dist/app-planner.d.ts +78 -0
- package/dist/app-planner.d.ts.map +1 -0
- package/dist/app-planner.js +145 -0
- package/dist/app-planner.js.map +1 -0
- package/dist/embeddings/provider.d.ts +66 -0
- package/dist/embeddings/provider.d.ts.map +1 -0
- package/dist/embeddings/provider.js +105 -0
- package/dist/embeddings/provider.js.map +1 -0
- package/dist/hints/git-store.d.ts +69 -0
- package/dist/hints/git-store.d.ts.map +1 -0
- package/dist/hints/git-store.js +244 -0
- package/dist/hints/git-store.js.map +1 -0
- package/dist/hints/retrieval.d.ts +49 -0
- package/dist/hints/retrieval.d.ts.map +1 -0
- package/dist/hints/retrieval.js +60 -0
- package/dist/hints/retrieval.js.map +1 -0
- package/dist/hints/store.d.ts +39 -0
- package/dist/hints/store.d.ts.map +1 -0
- package/dist/hints/store.js +279 -0
- package/dist/hints/store.js.map +1 -0
- package/dist/hints/types.d.ts +147 -0
- package/dist/hints/types.d.ts.map +1 -0
- package/dist/hints/types.js +92 -0
- package/dist/hints/types.js.map +1 -0
- package/dist/index.d.ts +43 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -3
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +63 -0
- package/dist/intent-controller.d.ts.map +1 -0
- package/dist/intent-controller.js +150 -0
- package/dist/intent-controller.js.map +1 -0
- package/dist/kg/build.d.ts.map +1 -1
- package/dist/kg/build.js +2 -0
- package/dist/kg/build.js.map +1 -1
- package/dist/kg/sqlite-fts.js +4 -0
- package/dist/kg/sqlite-fts.js.map +1 -1
- package/dist/kg/types.d.ts +10 -0
- package/dist/kg/types.d.ts.map +1 -1
- package/dist/metadata/catalog.d.ts +81 -4
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +208 -1
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/drafts.d.ts +22 -0
- package/dist/metadata/drafts.d.ts.map +1 -1
- package/dist/metadata/drafts.js +126 -2
- package/dist/metadata/drafts.js.map +1 -1
- package/dist/metadata/grain-gate.d.ts +61 -0
- package/dist/metadata/grain-gate.d.ts.map +1 -0
- package/dist/metadata/grain-gate.js +265 -0
- package/dist/metadata/grain-gate.js.map +1 -0
- package/dist/metadata/identity.d.ts +47 -0
- package/dist/metadata/identity.d.ts.map +1 -0
- package/dist/metadata/identity.js +147 -0
- package/dist/metadata/identity.js.map +1 -0
- package/dist/metadata/metric-match.d.ts +93 -0
- package/dist/metadata/metric-match.d.ts.map +1 -0
- package/dist/metadata/metric-match.js +233 -0
- package/dist/metadata/metric-match.js.map +1 -0
- package/dist/metadata/sql-grounding.d.ts +149 -0
- package/dist/metadata/sql-grounding.d.ts.map +1 -0
- package/dist/metadata/sql-grounding.js +397 -0
- package/dist/metadata/sql-grounding.js.map +1 -0
- package/dist/metadata/sql-retrieval.d.ts +29 -0
- package/dist/metadata/sql-retrieval.d.ts.map +1 -0
- package/dist/metadata/sql-retrieval.js +79 -0
- package/dist/metadata/sql-retrieval.js.map +1 -0
- package/dist/narrate.d.ts +52 -0
- package/dist/narrate.d.ts.map +1 -0
- package/dist/narrate.js +263 -0
- package/dist/narrate.js.map +1 -0
- package/dist/propose/build-from-prompt.d.ts +160 -0
- package/dist/propose/build-from-prompt.d.ts.map +1 -0
- package/dist/propose/build-from-prompt.js +815 -0
- package/dist/propose/build-from-prompt.js.map +1 -0
- package/dist/propose/classify.d.ts +55 -0
- package/dist/propose/classify.d.ts.map +1 -0
- package/dist/propose/classify.js +167 -0
- package/dist/propose/classify.js.map +1 -0
- package/dist/propose/config.d.ts +39 -0
- package/dist/propose/config.d.ts.map +1 -0
- package/dist/propose/config.js +52 -0
- package/dist/propose/config.js.map +1 -0
- package/dist/propose/dbt-artifacts.d.ts +134 -0
- package/dist/propose/dbt-artifacts.d.ts.map +1 -0
- package/dist/propose/dbt-artifacts.js +320 -0
- package/dist/propose/dbt-artifacts.js.map +1 -0
- package/dist/propose/edit-block.d.ts +74 -0
- package/dist/propose/edit-block.d.ts.map +1 -0
- package/dist/propose/edit-block.js +133 -0
- package/dist/propose/edit-block.js.map +1 -0
- package/dist/propose/enrich.d.ts +48 -0
- package/dist/propose/enrich.d.ts.map +1 -0
- package/dist/propose/enrich.js +125 -0
- package/dist/propose/enrich.js.map +1 -0
- package/dist/propose/generate-sql.d.ts +61 -0
- package/dist/propose/generate-sql.d.ts.map +1 -0
- package/dist/propose/generate-sql.js +180 -0
- package/dist/propose/generate-sql.js.map +1 -0
- package/dist/propose/index.d.ts +24 -0
- package/dist/propose/index.d.ts.map +1 -0
- package/dist/propose/index.js +15 -0
- package/dist/propose/index.js.map +1 -0
- package/dist/propose/propose.d.ts +226 -0
- package/dist/propose/propose.d.ts.map +1 -0
- package/dist/propose/propose.js +668 -0
- package/dist/propose/propose.js.map +1 -0
- package/dist/propose/reflect-block.d.ts +99 -0
- package/dist/propose/reflect-block.d.ts.map +1 -0
- package/dist/propose/reflect-block.js +220 -0
- package/dist/propose/reflect-block.js.map +1 -0
- package/dist/propose/write-draft.d.ts +90 -0
- package/dist/propose/write-draft.d.ts.map +1 -0
- package/dist/propose/write-draft.js +185 -0
- package/dist/propose/write-draft.js.map +1 -0
- package/dist/providers/claude.d.ts +13 -1
- package/dist/providers/claude.d.ts.map +1 -1
- package/dist/providers/claude.js +20 -2
- package/dist/providers/claude.js.map +1 -1
- package/dist/providers/gemini.d.ts +13 -1
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +24 -3
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/index.d.ts +3 -3
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +3 -3
- package/dist/providers/index.js.map +1 -1
- package/dist/research-loop.d.ts +62 -0
- package/dist/research-loop.d.ts.map +1 -0
- package/dist/research-loop.js +210 -0
- package/dist/research-loop.js.map +1 -0
- package/dist/skills/defaults.d.ts +37 -0
- package/dist/skills/defaults.d.ts.map +1 -0
- package/dist/skills/defaults.js +152 -0
- package/dist/skills/defaults.js.map +1 -0
- package/dist/skills/loader.d.ts +60 -0
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +164 -2
- package/dist/skills/loader.js.map +1 -1
- package/package.json +7 -5
package/dist/answer-loop.d.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* step. Tests can mock the provider with a canned response and exercise the
|
|
15
15
|
* full pipeline.
|
|
16
16
|
*/
|
|
17
|
+
import { type ResolvedTrustLabel } from '@duckcodeailabs/dql-core';
|
|
17
18
|
import type { KGStore } from './kg/sqlite-fts.js';
|
|
18
19
|
import type { KGNode, KGSearchHit } from './kg/types.js';
|
|
19
20
|
import type { AgentProvider } from './providers/types.js';
|
|
@@ -21,8 +22,22 @@ import type { Skill } from './skills/loader.js';
|
|
|
21
22
|
import type { AgentMemory } from './memory/sqlite-memory.js';
|
|
22
23
|
import type { LocalContextPack, MetadataAgentIntent } from './metadata/catalog.js';
|
|
23
24
|
import type { GeneratedDraftBlock } from './metadata/drafts.js';
|
|
25
|
+
import { type MetricMatch } from './metadata/metric-match.js';
|
|
26
|
+
import { type IntentDecision } from './intent-controller.js';
|
|
24
27
|
export type AnswerKind = 'certified' | 'uncertified' | 'no_answer';
|
|
25
28
|
export type AnswerSourceTier = 'certified_artifact' | 'business_context' | 'semantic_layer' | 'dbt_manifest' | 'no_answer';
|
|
29
|
+
/**
|
|
30
|
+
* The chosen route, surfaced on EVERY AI result (spec 17, part C) so the UI can
|
|
31
|
+
* show "where the answer came from". `tier` is the coarse route bucket; `ref`
|
|
32
|
+
* names the governed artifact/metric used (e.g. `cumulative_revenue`); `label`
|
|
33
|
+
* is a ready-to-render sentence.
|
|
34
|
+
*/
|
|
35
|
+
export type AiRouteTier = 'certified_block' | 'semantic_metric' | 'generated_sql' | 'business_context' | 'no_answer';
|
|
36
|
+
export interface AiRoute {
|
|
37
|
+
tier: AiRouteTier;
|
|
38
|
+
label: string;
|
|
39
|
+
ref?: string;
|
|
40
|
+
}
|
|
26
41
|
export type AnswerCertification = 'certified' | 'ai_generated' | 'analyst_review_required';
|
|
27
42
|
export type AnswerReviewStatus = 'none' | 'draft_ready' | 'analyst_review_required' | 'certified';
|
|
28
43
|
export type AgentIntent = MetadataAgentIntent | 'ad_hoc_analysis' | 'drillthrough';
|
|
@@ -157,6 +172,12 @@ export interface AgentAnswer {
|
|
|
157
172
|
certification?: AnswerCertification;
|
|
158
173
|
reviewStatus?: AnswerReviewStatus;
|
|
159
174
|
confidence?: number;
|
|
175
|
+
/**
|
|
176
|
+
* P0 intent controller — the high-level action the agent decided this turn
|
|
177
|
+
* deserves (answer / clarify / investigate / compose_app) with a rationale.
|
|
178
|
+
* Advisory: callers route on it (compose_app → app build, investigate → research).
|
|
179
|
+
*/
|
|
180
|
+
intentDecision?: IntentDecision;
|
|
160
181
|
/** Final answer text (NL summary). */
|
|
161
182
|
text: string;
|
|
162
183
|
/** Alias for UI envelopes. */
|
|
@@ -177,7 +198,15 @@ export interface AgentAnswer {
|
|
|
177
198
|
draftBlockId?: string;
|
|
178
199
|
draftBlock?: GeneratedDraftBlock;
|
|
179
200
|
promoteCommand?: string;
|
|
201
|
+
/** Legacy free-form trust label string, retained for backward compatibility. */
|
|
180
202
|
trustLabel?: string;
|
|
203
|
+
/**
|
|
204
|
+
* Canonical trust label (base + optional qualifier) drawn from the one shared
|
|
205
|
+
* vocabulary in dql-core, derived from this answer's source tier and review
|
|
206
|
+
* state. Lets every surface render the same label set as the MCP answer
|
|
207
|
+
* contract and the UI badge.
|
|
208
|
+
*/
|
|
209
|
+
trustLabelInfo?: ResolvedTrustLabel;
|
|
181
210
|
sourceCertifiedBlock?: string;
|
|
182
211
|
contextPackId?: string;
|
|
183
212
|
validationWarnings?: string[];
|
|
@@ -185,6 +214,8 @@ export interface AgentAnswer {
|
|
|
185
214
|
citations: AgentCitation[];
|
|
186
215
|
/** Relevant local memory supplied as advisory context. */
|
|
187
216
|
memoryContext?: AgentMemory[];
|
|
217
|
+
/** Approved Hint-Graph corrections that were applied to this answer (for transparency). */
|
|
218
|
+
appliedHints?: LocalContextPack['appliedHints'];
|
|
188
219
|
/** Evidence path connecting the question to metadata, SQL/block execution, and review state. */
|
|
189
220
|
evidence?: AgentEvidence;
|
|
190
221
|
/** Business-facing plan the agent used to answer the question. */
|
|
@@ -195,6 +226,24 @@ export interface AgentAnswer {
|
|
|
195
226
|
contextPack?: LocalContextPack;
|
|
196
227
|
/** Top KG hits the loop considered, useful for the UI's "we considered" panel. */
|
|
197
228
|
considered: KGSearchHit[];
|
|
229
|
+
/** The Skills that shaped this answer (selected, not all), for transparency. */
|
|
230
|
+
appliedSkills?: Array<{
|
|
231
|
+
id: string;
|
|
232
|
+
description?: string;
|
|
233
|
+
}>;
|
|
234
|
+
/**
|
|
235
|
+
* The chosen route (spec 17, part C). Surfaced on every result so the UI can
|
|
236
|
+
* show which tier answered (certified block, governed semantic metric,
|
|
237
|
+
* generated SQL, business context, or an honest refusal). Computed once at the
|
|
238
|
+
* single exit point in `answer()`.
|
|
239
|
+
*/
|
|
240
|
+
route?: AiRoute;
|
|
241
|
+
/**
|
|
242
|
+
* Internal: the governed metric the semantic tier matched (spec 17, part C).
|
|
243
|
+
* Used only to build `route` at the `answer()` exit point; not part of the
|
|
244
|
+
* stable public payload.
|
|
245
|
+
*/
|
|
246
|
+
_semanticMetricMatch?: MetricMatch;
|
|
198
247
|
}
|
|
199
248
|
export interface AgentResultPayload {
|
|
200
249
|
columns: unknown[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"answer-loop.d.ts","sourceRoot":"","sources":["../src/answer-loop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,MAAM,EAAc,WAAW,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAgB,MAAM,sBAAsB,CAAC;AACxE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAEhD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,mBAAmB,EAAyB,MAAM,uBAAuB,CAAC;AAC1G,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"answer-loop.d.ts","sourceRoot":"","sources":["../src/answer-loop.ts"],"names":[],"mappings":"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"}
|
package/dist/answer-loop.js
CHANGED
|
@@ -14,8 +14,12 @@
|
|
|
14
14
|
* step. Tests can mock the provider with a canned response and exercise the
|
|
15
15
|
* full pipeline.
|
|
16
16
|
*/
|
|
17
|
-
import {
|
|
17
|
+
import { composeEffectiveTrust, } from '@duckcodeailabs/dql-core';
|
|
18
|
+
import { buildSkillBlockHints, buildSkillsPrompt, selectRelevantSkills } from './skills/loader.js';
|
|
19
|
+
import { matchSemanticMetric, resolveGovernedMetricSql } from './metadata/metric-match.js';
|
|
20
|
+
import { decideAgentAction } from './intent-controller.js';
|
|
18
21
|
import { validateSqlAgainstLocalContext } from './metadata/sql-context-validation.js';
|
|
22
|
+
import { buildGroundingFromRuntimeRelations, resolveRelationsInSql, validateSqlAgainstGrounding } from './metadata/sql-grounding.js';
|
|
19
23
|
import { compactSqlSnippet, extractSimpleSelectShape, selectExpressionOutputName, } from './metadata/sql-shape.js';
|
|
20
24
|
const CERTIFIED_HIT_THRESHOLD = 0.18;
|
|
21
25
|
const HARD_NEGATIVE_RATIO = 0.5;
|
|
@@ -24,8 +28,112 @@ const BUSINESS_CONTEXT_KINDS = ['term', 'business_view'];
|
|
|
24
28
|
const ARTIFACT_KINDS = [...EXECUTABLE_ARTIFACT_KINDS, ...BUSINESS_CONTEXT_KINDS];
|
|
25
29
|
const SEMANTIC_KINDS = ['metric', 'dimension', 'measure', 'entity', 'semantic_model', 'saved_query'];
|
|
26
30
|
const MANIFEST_KINDS = ['dbt_model', 'dbt_source'];
|
|
31
|
+
/**
|
|
32
|
+
* Map an answer-loop result's source tier + certification + review state to a
|
|
33
|
+
* canonical trust-label id from the one shared vocabulary in dql-core.
|
|
34
|
+
* Additive and lenient — keeps the legacy `trustLabel` string untouched.
|
|
35
|
+
*/
|
|
36
|
+
function canonicalTrustLabelId(result) {
|
|
37
|
+
if (result.kind === 'no_answer')
|
|
38
|
+
return 'insufficient_context';
|
|
39
|
+
if (result.certification === 'certified' || result.kind === 'certified')
|
|
40
|
+
return 'certified';
|
|
41
|
+
if (result.sourceTier === 'business_context' && result.reviewStatus === 'certified')
|
|
42
|
+
return 'reviewed';
|
|
43
|
+
if (result.certification === 'ai_generated' ||
|
|
44
|
+
result.certification === 'analyst_review_required' ||
|
|
45
|
+
result.reviewStatus === 'analyst_review_required' ||
|
|
46
|
+
result.reviewStatus === 'draft_ready') {
|
|
47
|
+
return 'ai_generated';
|
|
48
|
+
}
|
|
49
|
+
return 'insufficient_context';
|
|
50
|
+
}
|
|
27
51
|
export async function answer(input) {
|
|
52
|
+
const result = await runAnswerLoop(input);
|
|
53
|
+
// Attach the canonical trust label once, at the single exit point, so every
|
|
54
|
+
// return site inside runAnswerLoop stays untouched and backward compatible.
|
|
55
|
+
// Freshness-aware trust: for a certified answer, fold the source block's data
|
|
56
|
+
// health (stale/failed upstream) into the label so it reads "Certified ·
|
|
57
|
+
// stale data" / "Certified · upstream failed". Non-certified or fresh answers
|
|
58
|
+
// are unaffected.
|
|
59
|
+
const id = canonicalTrustLabelId(result);
|
|
60
|
+
const dataState = id === 'certified'
|
|
61
|
+
? (result.block?.dataState)
|
|
62
|
+
: undefined;
|
|
63
|
+
const { _semanticMetricMatch, ...publicResult } = result;
|
|
64
|
+
const chosenRoute = result.route ?? deriveAiRoute(result, _semanticMetricMatch);
|
|
65
|
+
// P0 — record the high-level action this turn warranted, so callers can route
|
|
66
|
+
// (compose_app → app build, investigate → research) and the UI can show the
|
|
67
|
+
// agent's reasoning. Computed once at the single exit from the finished answer.
|
|
68
|
+
const tier = chosenRoute?.tier;
|
|
69
|
+
const intentDecision = decideAgentAction({
|
|
70
|
+
question: input.question,
|
|
71
|
+
intent: tier === 'no_answer' ? 'clarify' : 'ad_hoc_ranking',
|
|
72
|
+
signals: {
|
|
73
|
+
certifiedScore: tier === 'certified_block' ? 0.9 : 0,
|
|
74
|
+
metricScore: tier === 'semantic_metric' ? 0.9 : 0,
|
|
75
|
+
hasRetrieval: (result.considered?.length ?? 0) > 0,
|
|
76
|
+
missingContext: tier === 'no_answer' ? ['Need a clearer business object, measure, or grain before answering.'] : [],
|
|
77
|
+
},
|
|
78
|
+
isFollowUp: Boolean(input.followUp),
|
|
79
|
+
});
|
|
80
|
+
return {
|
|
81
|
+
...publicResult,
|
|
82
|
+
intentDecision,
|
|
83
|
+
trustLabelInfo: composeEffectiveTrust({ id, dataState }),
|
|
84
|
+
// Stamp the SELECTED skills that shaped the answer (transparency). Computed
|
|
85
|
+
// here so every return site inside runAnswerLoop stays untouched.
|
|
86
|
+
appliedSkills: result.appliedSkills ??
|
|
87
|
+
selectRelevantSkills(input.skills ?? [], input.question, { userId: input.userId ?? null }).map((s) => ({
|
|
88
|
+
id: s.id,
|
|
89
|
+
description: s.description,
|
|
90
|
+
})),
|
|
91
|
+
// Stamp the chosen route once, at the single exit point, so every return
|
|
92
|
+
// site inside runAnswerLoop stays untouched (spec 17, part C).
|
|
93
|
+
route: chosenRoute,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Derive the UI-facing route from a finished answer (spec 17, part C). The
|
|
98
|
+
* semantic-metric tier is named explicitly when the loop matched a governed
|
|
99
|
+
* metric; otherwise the route is mapped from the answer's source tier / kind.
|
|
100
|
+
*/
|
|
101
|
+
function deriveAiRoute(result, metricMatch) {
|
|
102
|
+
if (result.kind === 'no_answer') {
|
|
103
|
+
return { tier: 'no_answer', label: 'No governed answer — needs more context or review.' };
|
|
104
|
+
}
|
|
105
|
+
if (result.kind === 'certified') {
|
|
106
|
+
if (result.sourceTier === 'business_context') {
|
|
107
|
+
const ref = result.citations[0]?.name;
|
|
108
|
+
return {
|
|
109
|
+
tier: 'business_context',
|
|
110
|
+
label: ref ? `Answered from certified business context ${ref}` : 'Answered from certified business context',
|
|
111
|
+
ref,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const ref = result.sourceCertifiedBlock ?? result.block?.name ?? result.citations[0]?.name;
|
|
115
|
+
return {
|
|
116
|
+
tier: 'certified_block',
|
|
117
|
+
label: ref ? `Answered from certified block ${ref}` : 'Answered from a certified block',
|
|
118
|
+
ref,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
// Uncertified: a governed metric matched → semantic_metric; else generated SQL.
|
|
122
|
+
if (result.sourceTier === 'semantic_layer' && metricMatch) {
|
|
123
|
+
return {
|
|
124
|
+
tier: 'semantic_metric',
|
|
125
|
+
label: `Answered from metric ${metricMatch.metric.name}`,
|
|
126
|
+
ref: metricMatch.metric.name,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return { tier: 'generated_sql', label: 'Answered with generated SQL (review required).' };
|
|
130
|
+
}
|
|
131
|
+
async function runAnswerLoop(input) {
|
|
28
132
|
const { question, userId, domain, provider, kg, skills = [], blockHints = [] } = input;
|
|
133
|
+
// Select the RELEVANT skills (not all) for this question; keep pinned project
|
|
134
|
+
// skills (SQL conventions). Block hints still come from the full set so a
|
|
135
|
+
// preferred-block mapping is never lost.
|
|
136
|
+
const selectedSkills = selectRelevantSkills(skills, question, { userId: userId ?? null });
|
|
29
137
|
const effectiveBlockHints = Array.from(new Set([
|
|
30
138
|
...blockHints,
|
|
31
139
|
...buildSkillBlockHints(skills, userId ?? null),
|
|
@@ -148,7 +256,16 @@ export async function answer(input) {
|
|
|
148
256
|
providerUsed: provider.name,
|
|
149
257
|
};
|
|
150
258
|
}
|
|
151
|
-
|
|
259
|
+
// Spec 17, part C — SEMANTIC-METRIC MATCHING. Must run BEFORE the clarify
|
|
260
|
+
// short-circuit: FTS alone misses clear metric questions ("total revenue" never
|
|
261
|
+
// literally names a metric), so a clarify route would otherwise refuse a question
|
|
262
|
+
// a governed metric can answer. Match by name + synonyms + measure family + hybrid
|
|
263
|
+
// rank over the FTS semantic hits, then ALL metric KG nodes (revenue ⇄
|
|
264
|
+
// cumulative_revenue). Certified-first is still preserved (checked above).
|
|
265
|
+
const semanticMetricNodes = collectMetricCandidates(semanticHits, considered, kg);
|
|
266
|
+
let semanticMetricMatch = await matchSemanticMetric(question, semanticMetricNodes).catch(() => null);
|
|
267
|
+
// Clarify only when there is ALSO no confident governed-metric match.
|
|
268
|
+
if ((intent === 'clarify' || catalogRoute?.route === 'clarify') && !semanticMetricMatch) {
|
|
152
269
|
const text = composeCatalogClarificationText(question, catalogRoute) ?? composeClarificationText(question, considered, schemaContext);
|
|
153
270
|
const analysisPlan = buildAnalysisPlan({
|
|
154
271
|
question,
|
|
@@ -189,19 +306,27 @@ export async function answer(input) {
|
|
|
189
306
|
}
|
|
190
307
|
// Stage 2/3: generate only after certified artifacts miss. Semantic context
|
|
191
308
|
// wins over raw dbt manifest context; memory is appended last as advisory.
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
309
|
+
// A confident metric match forces the semantic tier even when FTS returned no
|
|
310
|
+
// semantic hits, so the governed metric (not refusal) answers the question.
|
|
311
|
+
const activeTier = sourceTierFromContextPack(input.contextPack)
|
|
312
|
+
?? (semanticHits.length > 0 || semanticMetricMatch
|
|
313
|
+
? 'semantic_layer'
|
|
314
|
+
: manifestHits.length > 0
|
|
315
|
+
? 'dbt_manifest'
|
|
316
|
+
: 'dbt_manifest');
|
|
197
317
|
const reviewRequiredArtifactHits = artifactHits
|
|
198
318
|
.filter((hit) => hit.score >= CERTIFIED_HIT_THRESHOLD && !isCertifiedHit(hit, kg))
|
|
199
319
|
.slice(0, 4);
|
|
200
320
|
const trustedArtifactContext = rankGeneratedContextHits(executableArtifactHits.filter((hit) => !excludedArtifactIds?.has(hit.node.nodeId)), schemaContext, question)
|
|
201
321
|
.filter((hit) => !excludedArtifactIds?.has(hit.node.nodeId))
|
|
202
322
|
.slice(0, 5);
|
|
323
|
+
// When a governed metric matched (spec 17, part C), pin it at the front of the
|
|
324
|
+
// semantic context so the generated SQL is grounded on the metric definition.
|
|
325
|
+
const matchedMetricHit = semanticMetricMatch
|
|
326
|
+
? [{ node: semanticMetricMatch.metric, score: Math.max(semanticMetricMatch.score, CERTIFIED_HIT_THRESHOLD) }]
|
|
327
|
+
: [];
|
|
203
328
|
const contextHits = activeTier === 'semantic_layer'
|
|
204
|
-
? [...trustedArtifactContext, ...reviewRequiredArtifactHits, ...businessHits.slice(0, 4), ...semanticHits, ...manifestHits].slice(0, 14)
|
|
329
|
+
? [...matchedMetricHit, ...trustedArtifactContext, ...reviewRequiredArtifactHits, ...businessHits.slice(0, 4), ...semanticHits, ...manifestHits].slice(0, 14)
|
|
205
330
|
: [...trustedArtifactContext, ...reviewRequiredArtifactHits, ...businessHits.slice(0, 4), ...manifestHits].slice(0, 14);
|
|
206
331
|
const contextNodes = mergeNodes(followUpSourceBlock && input.followUp?.kind === 'drilldown' ? [followUpSourceBlock] : [], (contextHits.length > 0 ? contextHits : considered.slice(0, 6)).map((h) => h.node));
|
|
207
332
|
const contextBlocks = contextNodes.filter((n) => n.kind === 'block');
|
|
@@ -210,7 +335,7 @@ export async function answer(input) {
|
|
|
210
335
|
const messages = [
|
|
211
336
|
{ role: 'system', content: SYSTEM_PROMPT },
|
|
212
337
|
];
|
|
213
|
-
const skillsPrompt = buildSkillsPrompt(
|
|
338
|
+
const skillsPrompt = buildSkillsPrompt(selectedSkills, userId ?? null);
|
|
214
339
|
if (skillsPrompt)
|
|
215
340
|
messages.push({ role: 'system', content: skillsPrompt });
|
|
216
341
|
messages.push({
|
|
@@ -267,6 +392,35 @@ export async function answer(input) {
|
|
|
267
392
|
}
|
|
268
393
|
parsed = parseProposal(proposed);
|
|
269
394
|
}
|
|
395
|
+
// True when `parsed.sql` was synthesized deterministically from a governed
|
|
396
|
+
// semantic-layer metric (not the LLM). Such SQL is trusted and grounded against
|
|
397
|
+
// the runtime schema, so it skips the hallucination-guard context validation
|
|
398
|
+
// that exists to catch model-invented relations/columns.
|
|
399
|
+
let governedMetricAnswer = false;
|
|
400
|
+
if (!parsed.sql) {
|
|
401
|
+
// Spec 17, part C — if a governed metric matched confidently but the model
|
|
402
|
+
// declined SQL, answer from the metric definition (deterministic, offline)
|
|
403
|
+
// rather than refusing. The semantic tier is the governed answer here. A
|
|
404
|
+
// derived MetricFlow metric (e.g. `revenue`) often carries no executable
|
|
405
|
+
// definition itself — its `table:`/`sql:` live on the backing measure node
|
|
406
|
+
// (`order_item.revenue`). resolveGovernedMetricSql resolves a thin metric to
|
|
407
|
+
// that synthesizable sibling so the route lands on a real number, not refusal.
|
|
408
|
+
const resolved = activeTier === 'semantic_layer' && semanticMetricMatch
|
|
409
|
+
? resolveGovernedMetricSql(semanticMetricMatch.metric, semanticMetricNodes)
|
|
410
|
+
: undefined;
|
|
411
|
+
if (resolved) {
|
|
412
|
+
// Point the match at the metric we actually answered from so the route
|
|
413
|
+
// badge, citation, and carrier all reflect the executable definition used.
|
|
414
|
+
semanticMetricMatch = { ...semanticMetricMatch, metric: resolved.metric };
|
|
415
|
+
governedMetricAnswer = true;
|
|
416
|
+
parsed = {
|
|
417
|
+
sql: resolved.sql,
|
|
418
|
+
text: parsed.text ||
|
|
419
|
+
`Answered from the governed metric ${resolved.metric.name}. This result is uncertified until reviewed and promoted.`,
|
|
420
|
+
viz: parsed.viz ?? 'single_value',
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
}
|
|
270
424
|
if (!parsed.sql) {
|
|
271
425
|
const text = parsed.text || 'No answer (the model declined to propose SQL).';
|
|
272
426
|
return {
|
|
@@ -294,11 +448,69 @@ export async function answer(input) {
|
|
|
294
448
|
providerUsed: provider.name,
|
|
295
449
|
};
|
|
296
450
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
451
|
+
// Shared grounding (spec 15): deterministically qualify any bare relation the
|
|
452
|
+
// model emitted to its real warehouse relation from the runtime schema BEFORE
|
|
453
|
+
// governance validation. Same resolver the build path uses — one grounding,
|
|
454
|
+
// no weak path. `allowedSqlContext` relations are already qualified.
|
|
455
|
+
let grounding;
|
|
456
|
+
if (parsed.sql && schemaContext.length > 0) {
|
|
457
|
+
grounding = buildGroundingFromRuntimeRelations(schemaContext.map((table) => ({
|
|
458
|
+
relation: table.relation,
|
|
459
|
+
name: table.name,
|
|
460
|
+
columns: table.columns.map((column) => ({ name: column.name, type: column.type, description: column.description })),
|
|
461
|
+
})));
|
|
462
|
+
parsed.sql = resolveRelationsInSql(parsed.sql, grounding, { prefer: 'qualified' }).sql;
|
|
463
|
+
}
|
|
464
|
+
// Validation gate. Governed metric SQL synthesized from the semantic layer is
|
|
465
|
+
// already trusted (deterministic + grounded); model SQL is validated against the
|
|
466
|
+
// inspected context to catch hallucinated relations/columns.
|
|
467
|
+
const semanticMetricRoute = activeTier === 'semantic_layer' && Boolean(semanticMetricMatch);
|
|
468
|
+
let contextValidation;
|
|
469
|
+
if (governedMetricAnswer) {
|
|
470
|
+
contextValidation = { ok: true, warnings: [] };
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
const c = validateSqlAgainstLocalContext(parsed.sql, input.contextPack, {
|
|
474
|
+
question,
|
|
475
|
+
intent,
|
|
476
|
+
filterValues: input.followUp?.filters,
|
|
477
|
+
});
|
|
478
|
+
contextValidation = c.ok
|
|
479
|
+
? { ok: true, warnings: c.warnings }
|
|
480
|
+
: { ok: false, error: c.error, warnings: c.warnings };
|
|
481
|
+
}
|
|
482
|
+
// Spec 17, part C — semantic-metric route recovery. A metric question is often
|
|
483
|
+
// catalog-routed to clarify, leaving a thin contextPack that rejects otherwise-valid
|
|
484
|
+
// SQL. Rather than refuse, recover in two steps:
|
|
485
|
+
// 1) Re-judge the model's own SQL against the RUNTIME grounding. If it references
|
|
486
|
+
// real relations/columns it is valid (the contextPack rejection was a false
|
|
487
|
+
// negative) — keep the model SQL so a precise answer like `count(*) FROM orders`
|
|
488
|
+
// stands instead of guessing a measure.
|
|
489
|
+
// 2) Otherwise fall back to a CLEAN governed-metric definition (direct or exact
|
|
490
|
+
// leaf-measure; no fuzzy family guess that could answer the wrong measure).
|
|
491
|
+
if (!contextValidation.ok && semanticMetricRoute && semanticMetricMatch) {
|
|
492
|
+
const grounded = grounding ? validateSqlAgainstGrounding(parsed.sql, grounding) : undefined;
|
|
493
|
+
if (grounded?.ok) {
|
|
494
|
+
contextValidation = { ok: true, warnings: grounded.warnings };
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
const recovered = resolveGovernedMetricSql(semanticMetricMatch.metric, semanticMetricNodes);
|
|
498
|
+
if (recovered) {
|
|
499
|
+
semanticMetricMatch = { ...semanticMetricMatch, metric: recovered.metric };
|
|
500
|
+
parsed.sql = grounding
|
|
501
|
+
? resolveRelationsInSql(recovered.sql, grounding, { prefer: 'qualified' }).sql
|
|
502
|
+
: recovered.sql;
|
|
503
|
+
parsed.viz = parsed.viz ?? 'single_value';
|
|
504
|
+
parsed.text = parsed.text
|
|
505
|
+
|| `Answered from the governed metric ${recovered.metric.name}. This result is uncertified until reviewed and promoted.`;
|
|
506
|
+
governedMetricAnswer = true;
|
|
507
|
+
contextValidation = {
|
|
508
|
+
ok: true,
|
|
509
|
+
warnings: ['Generated SQL failed context validation; answered from the governed metric definition instead.'],
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
302
514
|
if (!contextValidation.ok) {
|
|
303
515
|
const text = `I could not safely prepare this generated SQL from the inspected context. ${contextValidation.error}`;
|
|
304
516
|
const analysisPlan = buildAnalysisPlan({
|
|
@@ -514,6 +726,9 @@ export async function answer(input) {
|
|
|
514
726
|
contextPack: input.contextPack,
|
|
515
727
|
considered,
|
|
516
728
|
providerUsed: localProposal ? 'schema_planner' : provider.name,
|
|
729
|
+
// Carry the governed metric match so the exit point can name a
|
|
730
|
+
// `semantic_metric` route (spec 17, part C).
|
|
731
|
+
_semanticMetricMatch: activeTier === 'semantic_layer' ? semanticMetricMatch ?? undefined : undefined,
|
|
517
732
|
};
|
|
518
733
|
}
|
|
519
734
|
const SYSTEM_PROMPT = `You are the DQL Analytics Agent.
|
|
@@ -590,6 +805,17 @@ function renderContextPrompt(blocks, businessContext, others, activeTier, memory
|
|
|
590
805
|
.map((m) => `- ${m.scope}${m.scopeId ? `:${m.scopeId}` : ''} \`${m.title}\` (${m.source}, confidence ${m.confidence}): ${m.content}`)
|
|
591
806
|
.join('\n')}`
|
|
592
807
|
: '';
|
|
808
|
+
// Approved, scope-matched correction hints from the Hint Graph. These are
|
|
809
|
+
// human-reviewed lessons (often from a prior wrong→right SQL correction), so
|
|
810
|
+
// they carry more weight than advisory memory when generating SQL — but they
|
|
811
|
+
// still must NOT override a certified artifact that already answers exactly.
|
|
812
|
+
const appliedHints = contextPack?.appliedHints ?? [];
|
|
813
|
+
const hintsSection = appliedHints.length > 0
|
|
814
|
+
? `\n\n## Applied governed hints (human-approved corrections)\n\nReviewed, scope-matched corrections from your team. Apply them when generating SQL to avoid known mistakes; they refine generated SQL but MUST NOT override a certified artifact that already answers the question.\n${appliedHints
|
|
815
|
+
.slice(0, 6)
|
|
816
|
+
.map((h) => `- \`${h.title}\`: ${h.guidance}${h.correctedSql ? `\n corrected SQL pattern: ${h.correctedSql.replace(/\s+/g, ' ').trim().slice(0, 240)}` : ''}`)
|
|
817
|
+
.join('\n')}`
|
|
818
|
+
: '';
|
|
593
819
|
const extraSection = extraContext?.trim()
|
|
594
820
|
? `\n\n## Current notebook/app context\n\nThis context may help interpret the user's request, but it MUST NOT override certified artifacts, semantic metrics, dbt metadata, or generated SQL validation.\n\n${extraContext.trim()}`
|
|
595
821
|
: '';
|
|
@@ -599,7 +825,7 @@ function renderContextPrompt(blocks, businessContext, others, activeTier, memory
|
|
|
599
825
|
const contextPackSection = contextPack
|
|
600
826
|
? `\n\n## Local metadata context pack\n\n${renderContextPackForPrompt(contextPack)}`
|
|
601
827
|
: '';
|
|
602
|
-
return `${intentSection}\n\n${blockSection}${businessSection}${otherSection}${schemaSection}${contextPackSection}${memorySection}${extraSection}${followUpSection}`;
|
|
828
|
+
return `${intentSection}\n\n${blockSection}${businessSection}${otherSection}${schemaSection}${contextPackSection}${memorySection}${hintsSection}${extraSection}${followUpSection}`;
|
|
603
829
|
}
|
|
604
830
|
function renderContextPackForPrompt(contextPack) {
|
|
605
831
|
const questionPlan = contextPack.questionPlan
|
|
@@ -718,14 +944,19 @@ function renderSourceBlockSqlContext(contextPack) {
|
|
|
718
944
|
.join(', ')
|
|
719
945
|
: '';
|
|
720
946
|
const snippet = compactSqlSnippet(source.sql, 280);
|
|
947
|
+
// Pair each certified block's SQL with the natural-language question it
|
|
948
|
+
// answers so it reads as a question→SQL few-shot exemplar (DAIL-SQL).
|
|
949
|
+
const anchorRaw = source.exampleQuestion ?? source.description;
|
|
950
|
+
const anchor = anchorRaw ? anchorRaw.replace(/\s+/g, ' ').trim().slice(0, 160) : '';
|
|
721
951
|
return [
|
|
722
|
-
`- ${source.name}${source.status ? ` (${source.status})` : ''}`,
|
|
952
|
+
`- ${source.name}${source.status ? ` (${source.status})` : ''}${source.grain ? ` — grain: ${source.grain}` : ''}`,
|
|
953
|
+
anchor ? ` answers: ${anchor}` : '',
|
|
723
954
|
shape?.relation ? ` relation: ${shape.relation}` : '',
|
|
724
955
|
projectedColumns ? ` projected columns: ${projectedColumns}` : '',
|
|
725
956
|
snippet ? ` sql: ${snippet}` : '',
|
|
726
957
|
].filter(Boolean).join('\n');
|
|
727
958
|
});
|
|
728
|
-
return
|
|
959
|
+
return `## Worked examples from certified blocks (few-shot patterns)\n\nThese certified blocks already answer similar questions. Learn their join paths, grain, and filters and ADAPT them to the question — do not copy blindly, and do not relabel generated SQL as certified.\n${lines.join('\n')}`;
|
|
729
960
|
}
|
|
730
961
|
function contextPackCitations(contextPack, limit) {
|
|
731
962
|
if (!contextPack)
|
|
@@ -2789,9 +3020,30 @@ function composeCertifiedAnswer(artifact, question, result, executionError) {
|
|
|
2789
3020
|
: artifact.kind === 'block'
|
|
2790
3021
|
? 'Governed execution was not requested by this host.'
|
|
2791
3022
|
: `Matched certified ${artifact.kind.replace('_', ' ')} context.`;
|
|
3023
|
+
// Freshness-aware trust: the block's logic is certified, but its upstream
|
|
3024
|
+
// data may be stale or its last dbt run may have failed. Caveat the answer so
|
|
3025
|
+
// a consumer can weigh it — "certified" is not the same as "fresh".
|
|
3026
|
+
const freshnessCaveat = certifiedFreshnessCaveat(artifact);
|
|
2792
3027
|
return `Outcome: Reuse certified block\n\nAnswered by certified ${artifact.kind.replace('_', ' ')} **${artifact.name}**${tag}.\n\n${desc ? `${desc}\n\n${resultText}` : resultText}`
|
|
3028
|
+
+ (freshnessCaveat ? `\n\n${freshnessCaveat}` : '')
|
|
2793
3029
|
+ `\n\n_Question:_ ${question}`;
|
|
2794
3030
|
}
|
|
3031
|
+
/**
|
|
3032
|
+
* Build a one-line data-freshness caveat for a certified answer, or `undefined`
|
|
3033
|
+
* when the upstream data is fresh / un-instrumented. Stale and failed upstreams
|
|
3034
|
+
* are surfaced so a certified-but-stale answer is never presented as if its data
|
|
3035
|
+
* were current.
|
|
3036
|
+
*/
|
|
3037
|
+
function certifiedFreshnessCaveat(artifact) {
|
|
3038
|
+
switch (artifact.dataState) {
|
|
3039
|
+
case 'failed':
|
|
3040
|
+
return `⚠️ Data caveat: an upstream dbt model's last run failed, so this certified result may be missing or out of date.${artifact.dataStateDetail ? ` ${artifact.dataStateDetail}` : ''}`;
|
|
3041
|
+
case 'stale':
|
|
3042
|
+
return `⚠️ Data caveat: upstream data is past its freshness window, so this certified result may be stale.${artifact.dataStateDetail ? ` ${artifact.dataStateDetail}` : ''}`;
|
|
3043
|
+
default:
|
|
3044
|
+
return undefined;
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
2795
3047
|
function mergeHits(...groups) {
|
|
2796
3048
|
const byId = new Map();
|
|
2797
3049
|
for (const group of groups) {
|
|
@@ -2813,6 +3065,29 @@ function mergeNodes(...groups) {
|
|
|
2813
3065
|
}
|
|
2814
3066
|
return Array.from(byId.values());
|
|
2815
3067
|
}
|
|
3068
|
+
/**
|
|
3069
|
+
* Candidate metric KG nodes for semantic-metric matching (spec 17, part C).
|
|
3070
|
+
* Starts with the FTS semantic + considered hits, then folds in EVERY metric
|
|
3071
|
+
* node from the KG so a confident measure-family match is found even when FTS
|
|
3072
|
+
* surfaced no metric at all (the "total revenue" miss). Bounded for safety.
|
|
3073
|
+
*/
|
|
3074
|
+
function collectMetricCandidates(semanticHits, considered, kg) {
|
|
3075
|
+
const byId = new Map();
|
|
3076
|
+
for (const hit of [...semanticHits, ...considered]) {
|
|
3077
|
+
if (hit.node.kind === 'metric')
|
|
3078
|
+
byId.set(hit.node.nodeId, hit.node);
|
|
3079
|
+
}
|
|
3080
|
+
try {
|
|
3081
|
+
for (const node of kg.getNodesByKind('metric', 200)) {
|
|
3082
|
+
if (!byId.has(node.nodeId))
|
|
3083
|
+
byId.set(node.nodeId, node);
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
catch {
|
|
3087
|
+
// Best-effort: a KG without a getNodesByKind still matches the FTS hits.
|
|
3088
|
+
}
|
|
3089
|
+
return Array.from(byId.values());
|
|
3090
|
+
}
|
|
2816
3091
|
function buildCertifiedEvidence(input) {
|
|
2817
3092
|
const businessContextAssets = uniqueAssets(input.businessHits
|
|
2818
3093
|
.map((hit) => hit.node)
|