@duckcodeailabs/dql-agent 1.6.32 → 1.6.34
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 +26 -8
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +46 -7
- 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 +15 -3
- 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 +14 -3
- package/dist/agent-run-planner.js.map +1 -1
- package/dist/agentic/answer-contract.d.ts +37 -0
- package/dist/agentic/answer-contract.d.ts.map +1 -0
- package/dist/agentic/answer-contract.js +47 -0
- package/dist/agentic/answer-contract.js.map +1 -0
- package/dist/agentic/tool-loop.d.ts +48 -0
- package/dist/agentic/tool-loop.d.ts.map +1 -0
- package/dist/agentic/tool-loop.js +181 -0
- package/dist/agentic/tool-loop.js.map +1 -0
- package/dist/agentic/toolset.d.ts +40 -0
- package/dist/agentic/toolset.d.ts.map +1 -0
- package/dist/agentic/toolset.js +265 -0
- package/dist/agentic/toolset.js.map +1 -0
- package/dist/answer-loop.d.ts +25 -2
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +368 -72
- package/dist/answer-loop.js.map +1 -1
- package/dist/cascade/budgets.d.ts +20 -2
- package/dist/cascade/budgets.d.ts.map +1 -1
- package/dist/cascade/budgets.js +76 -43
- package/dist/cascade/budgets.js.map +1 -1
- package/dist/cascade/route-policy.d.ts +1 -1
- package/dist/cascade/route-policy.d.ts.map +1 -1
- package/dist/cascade/route-policy.js +1 -0
- package/dist/cascade/route-policy.js.map +1 -1
- package/dist/embeddings/provider.d.ts +67 -4
- package/dist/embeddings/provider.d.ts.map +1 -1
- package/dist/embeddings/provider.js +167 -6
- package/dist/embeddings/provider.js.map +1 -1
- package/dist/hints/correction-eval.d.ts +30 -0
- package/dist/hints/correction-eval.d.ts.map +1 -0
- package/dist/hints/correction-eval.js +63 -0
- package/dist/hints/correction-eval.js.map +1 -0
- package/dist/hints/staleness.d.ts +30 -0
- package/dist/hints/staleness.d.ts.map +1 -0
- package/dist/hints/staleness.js +38 -0
- package/dist/hints/staleness.js.map +1 -0
- package/dist/hints/store.d.ts.map +1 -1
- package/dist/hints/store.js +3 -32
- package/dist/hints/store.js.map +1 -1
- package/dist/index.d.ts +39 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +143 -7
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js +15 -3
- package/dist/intent-controller.js.map +1 -1
- package/dist/kg/sqlite-fts.d.ts +13 -0
- package/dist/kg/sqlite-fts.d.ts.map +1 -1
- package/dist/kg/sqlite-fts.js +75 -47
- package/dist/kg/sqlite-fts.js.map +1 -1
- package/dist/memory/fts-query.d.ts +37 -4
- package/dist/memory/fts-query.d.ts.map +1 -1
- package/dist/memory/fts-query.js +53 -12
- package/dist/memory/fts-query.js.map +1 -1
- package/dist/metadata/analysis-planner.js +45 -10
- package/dist/metadata/analysis-planner.js.map +1 -1
- package/dist/metadata/block-adapt.d.ts +37 -0
- package/dist/metadata/block-adapt.d.ts.map +1 -0
- package/dist/metadata/block-adapt.js +89 -0
- package/dist/metadata/block-adapt.js.map +1 -0
- package/dist/metadata/catalog.d.ts +39 -0
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +361 -41
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/example-match.d.ts +30 -0
- package/dist/metadata/example-match.d.ts.map +1 -0
- package/dist/metadata/example-match.js +69 -0
- package/dist/metadata/example-match.js.map +1 -0
- package/dist/metadata/grain-ledger.d.ts +51 -0
- package/dist/metadata/grain-ledger.d.ts.map +1 -0
- package/dist/metadata/grain-ledger.js +174 -0
- package/dist/metadata/grain-ledger.js.map +1 -0
- package/dist/metadata/metric-match.d.ts +21 -1
- package/dist/metadata/metric-match.d.ts.map +1 -1
- package/dist/metadata/metric-match.js +157 -21
- package/dist/metadata/metric-match.js.map +1 -1
- package/dist/metadata/sql-grounding.d.ts +4 -2
- package/dist/metadata/sql-grounding.d.ts.map +1 -1
- package/dist/metadata/sql-grounding.js +4 -2
- package/dist/metadata/sql-grounding.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 +23 -5
- package/dist/metadata/sql-retrieval.js.map +1 -1
- package/dist/propose/build-from-prompt.d.ts.map +1 -1
- package/dist/propose/build-from-prompt.js +10 -4
- package/dist/propose/build-from-prompt.js.map +1 -1
- package/dist/propose/failure-analysis.d.ts +48 -0
- package/dist/propose/failure-analysis.d.ts.map +1 -0
- package/dist/propose/failure-analysis.js +71 -0
- package/dist/propose/failure-analysis.js.map +1 -0
- package/dist/propose/join-mining.d.ts +18 -0
- package/dist/propose/join-mining.d.ts.map +1 -0
- package/dist/propose/join-mining.js +74 -0
- package/dist/propose/join-mining.js.map +1 -0
- package/dist/providers/claude.d.ts.map +1 -1
- package/dist/providers/claude.js +36 -1
- package/dist/providers/claude.js.map +1 -1
- package/dist/providers/index.d.ts +2 -2
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +34 -1
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/reasoning-effort.d.ts +27 -0
- package/dist/providers/reasoning-effort.d.ts.map +1 -1
- package/dist/providers/reasoning-effort.js +32 -0
- package/dist/providers/reasoning-effort.js.map +1 -1
- package/dist/providers/types.d.ts +2 -1
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/research-loop.d.ts.map +1 -1
- package/dist/research-loop.js +9 -2
- package/dist/research-loop.js.map +1 -1
- package/dist/semantic-bridge/compose.d.ts.map +1 -1
- package/dist/semantic-bridge/compose.js +147 -20
- package/dist/semantic-bridge/compose.js.map +1 -1
- package/dist/skills/bootstrap.d.ts +57 -0
- package/dist/skills/bootstrap.d.ts.map +1 -0
- package/dist/skills/bootstrap.js +260 -0
- package/dist/skills/bootstrap.js.map +1 -0
- package/dist/skills/defaults.d.ts +2 -2
- package/dist/skills/defaults.js +2 -2
- package/dist/skills/domain-skills.d.ts +24 -0
- package/dist/skills/domain-skills.d.ts.map +1 -0
- package/dist/skills/domain-skills.js +76 -0
- package/dist/skills/domain-skills.js.map +1 -0
- package/dist/skills/loader.d.ts +46 -8
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +186 -41
- package/dist/skills/loader.js.map +1 -1
- package/dist/tools/registry.js +12 -12
- package/dist/tools/registry.js.map +1 -1
- package/package.json +4 -4
package/dist/answer-loop.js
CHANGED
|
@@ -14,22 +14,28 @@
|
|
|
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 { describeDialectForPrompt, } from '@duckcodeailabs/dql-core';
|
|
18
|
+
import { buildSkillBlockHints, buildSkillMetricHints, buildSkillsPrompt, expandQuestionWithSkillVocabulary, selectRelevantSkills } from './skills/loader.js';
|
|
18
19
|
import { deriveGeneratedDraftSlug, renderGeneratedSqlDqlArtifact } from './metadata/drafts.js';
|
|
19
20
|
import { buildAnalysisQuestionPlan } from './metadata/analysis-planner.js';
|
|
20
21
|
import { certifiedFitAllowsTier1, evaluateCertifiedBlockFit } from './metadata/block-fit.js';
|
|
21
|
-
import { buildGovernedMetricFirstSql, matchSemanticMetric, resolveGovernedMetricSql } from './metadata/metric-match.js';
|
|
22
|
+
import { buildGovernedMetricFirstSql, matchSemanticMetric, resolveGovernedMetricDefinition, resolveGovernedMetricSql } from './metadata/metric-match.js';
|
|
22
23
|
import { decideAgentAction } from './intent-controller.js';
|
|
23
24
|
import { createContextLedger } from './grounding/context-ledger.js';
|
|
24
25
|
import { validateAnswerResultShape } from './answer-shape.js';
|
|
26
|
+
import { fanoutWarningsForSql } from './metadata/grain-ledger.js';
|
|
27
|
+
import { planCertifiedAdaptation } from './metadata/block-adapt.js';
|
|
25
28
|
import { compactSqlSnippet, extractSimpleSelectShape, selectExpressionOutputName, } from './metadata/sql-shape.js';
|
|
26
29
|
import { composeSemanticQueryForQuestion, composeSemanticQueryFromMembers } from './semantic-bridge/compose.js';
|
|
30
|
+
import { runAgenticToolLoop } from './agentic/tool-loop.js';
|
|
31
|
+
import { buildSemanticStageTools } from './agentic/toolset.js';
|
|
32
|
+
import { deriveAgenticTrust } from './agentic/answer-contract.js';
|
|
27
33
|
import { selectSemanticMembersViaLlm } from './semantic-bridge/member-select.js';
|
|
28
34
|
import { normalizeValueIndexText } from './grounding/value-index.js';
|
|
29
35
|
import { cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, } from './cascade/cascade.js';
|
|
30
36
|
import { shouldClarifyBeforeGeneration } from './cascade/triage.js';
|
|
31
37
|
import { stampTrustLabel } from './trust/stamp.js';
|
|
32
|
-
import { QUICK_PROMPT_CONTEXT_BUDGET, canUseLaneRepair, cascadeBudgetTrace, createCascadeBudgetState, promptContextBudgetForQuestion, proposalToolBudgetForQuestion, recordLaneRepair, } from './cascade/budgets.js';
|
|
38
|
+
import { QUICK_PROMPT_CONTEXT_BUDGET, canUseLaneRepair, cascadeBudgetTrace, createCascadeBudgetState, deepAlternativeCountForQuestion, promptContextBudgetForQuestion, proposalToolBudgetForQuestion, recordLaneRepair, } from './cascade/budgets.js';
|
|
33
39
|
/**
|
|
34
40
|
* Physical column names whose sampled runtime values include `value`. Used by the
|
|
35
41
|
* semantic bridge to bind a filter literal to the dimension that actually carries
|
|
@@ -157,10 +163,12 @@ export async function answer(input) {
|
|
|
157
163
|
},
|
|
158
164
|
isFollowUp: Boolean(input.followUp),
|
|
159
165
|
});
|
|
166
|
+
const trustLabelInfo = stampTrustLabel(result);
|
|
160
167
|
return {
|
|
161
168
|
...publicResult,
|
|
162
169
|
intentDecision,
|
|
163
|
-
trustLabelInfo
|
|
170
|
+
trustLabelInfo,
|
|
171
|
+
provenanceFooter: buildProvenanceFooter(result, trustLabelInfo),
|
|
164
172
|
cascade: publicResult.cascade ?? createCascadeAnswerResult({
|
|
165
173
|
routeTier: chosenRoute.tier,
|
|
166
174
|
label: chosenRoute.label,
|
|
@@ -178,7 +186,13 @@ export async function answer(input) {
|
|
|
178
186
|
// Stamp the SELECTED skills that shaped the answer (transparency). Computed
|
|
179
187
|
// here so every return site inside runAnswerLoop stays untouched.
|
|
180
188
|
appliedSkills: result.appliedSkills ??
|
|
181
|
-
selectRelevantSkills(input.skills ?? [], input.question, {
|
|
189
|
+
selectRelevantSkills(input.skills ?? [], input.question, {
|
|
190
|
+
userId: input.userId ?? null,
|
|
191
|
+
domains: Array.from(new Set([
|
|
192
|
+
...(input.domain ? [input.domain] : []),
|
|
193
|
+
...(input.contextPack?.objects ?? []).slice(0, 20).flatMap((object) => object.domain ? [object.domain] : []),
|
|
194
|
+
])),
|
|
195
|
+
}).map((s) => ({
|
|
182
196
|
id: s.id,
|
|
183
197
|
description: s.description,
|
|
184
198
|
})),
|
|
@@ -240,11 +254,22 @@ async function runAnswerLoop(input) {
|
|
|
240
254
|
// Select the RELEVANT skills (not all) for this question; keep pinned project
|
|
241
255
|
// skills (SQL conventions). Block hints still come from the full set so a
|
|
242
256
|
// preferred-block mapping is never lost.
|
|
243
|
-
const
|
|
257
|
+
const inferredDomains = Array.from(new Set([
|
|
258
|
+
...(domain ? [domain] : []),
|
|
259
|
+
...(input.contextPack?.objects ?? []).slice(0, 20).flatMap((object) => object.domain ? [object.domain] : []),
|
|
260
|
+
]));
|
|
261
|
+
const selectedSkills = selectRelevantSkills(skills, question, {
|
|
262
|
+
userId: userId ?? null,
|
|
263
|
+
domains: inferredDomains,
|
|
264
|
+
});
|
|
244
265
|
const effectiveBlockHints = Array.from(new Set([
|
|
245
266
|
...blockHints,
|
|
246
|
-
|
|
267
|
+
// Only selected skills may influence block ranking. Previously a preferred
|
|
268
|
+
// block from any active but unrelated domain skill could jump to the front.
|
|
269
|
+
...buildSkillBlockHints(selectedSkills, userId ?? null),
|
|
247
270
|
]));
|
|
271
|
+
const effectiveMetricHints = buildSkillMetricHints(selectedSkills, userId ?? null);
|
|
272
|
+
const semanticQuestion = expandQuestionWithSkillVocabulary(question, selectedSkills, userId ?? null);
|
|
248
273
|
const followUpSourceBlock = input.followUp?.sourceBlockName
|
|
249
274
|
? kg.getNode(`block:${input.followUp.sourceBlockName}`)
|
|
250
275
|
: null;
|
|
@@ -272,6 +297,19 @@ async function runAnswerLoop(input) {
|
|
|
272
297
|
schemaContext,
|
|
273
298
|
});
|
|
274
299
|
let intent = catalogRoute ? agentIntentFromCatalogRoute(catalogRoute) : fallbackIntent;
|
|
300
|
+
// Resolve a governed metric before accepting a catalog-proposed certified
|
|
301
|
+
// block. Catalog token overlap can call a generic word such as "total" an
|
|
302
|
+
// exact block match (for example total tax → total revenue); the metric match
|
|
303
|
+
// is the precise measure signal that prevents that wrong Tier-1 shortcut.
|
|
304
|
+
const semanticMetricNodes = collectMetricCandidates(semanticHits, considered, kg);
|
|
305
|
+
for (const metric of effectiveMetricHints) {
|
|
306
|
+
const node = kg.getNode(`metric:${metric}`);
|
|
307
|
+
if (node && !semanticMetricNodes.some((candidate) => candidate.nodeId === node.nodeId))
|
|
308
|
+
semanticMetricNodes.push(node);
|
|
309
|
+
}
|
|
310
|
+
let semanticMetricMatch = await matchSemanticMetric(semanticQuestion, semanticMetricNodes, {
|
|
311
|
+
measureTerms: [...questionPlan.requestedShape.measures, ...questionPlan.metricTerms],
|
|
312
|
+
}).catch(() => null);
|
|
275
313
|
// Stage 1: certified artifact match. Blocks can be executed; dashboards,
|
|
276
314
|
// Apps, and notebooks are returned as governed citations/navigation targets.
|
|
277
315
|
const drilldownCertifiedHit = input.followUp?.kind === 'drilldown'
|
|
@@ -284,19 +322,29 @@ async function runAnswerLoop(input) {
|
|
|
284
322
|
kg,
|
|
285
323
|
})
|
|
286
324
|
: null;
|
|
287
|
-
const
|
|
325
|
+
const shouldTryCertifiedRoute = shouldUseCertifiedRoute(catalogRoute, intent);
|
|
326
|
+
const catalogCertifiedHit = shouldTryCertifiedRoute
|
|
288
327
|
? certifiedHitFromContextPack(input.contextPack, kg)
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
328
|
+
: null;
|
|
329
|
+
const unsafeCatalogCertifiedHit = catalogCertifiedHit?.node.kind === 'block' && semanticMetricMatch
|
|
330
|
+
&& !hasCertifiedNodeFit(question, questionPlan, catalogCertifiedHit.node)
|
|
331
|
+
? null
|
|
332
|
+
: catalogCertifiedHit;
|
|
333
|
+
const fallbackCertifiedHit = shouldTryCertifiedRoute ? pickCertifiedArtifact({
|
|
334
|
+
artifactHits,
|
|
335
|
+
executableArtifactHits,
|
|
336
|
+
businessHits,
|
|
337
|
+
question,
|
|
338
|
+
questionPlan,
|
|
339
|
+
blockHints: input.followUp?.kind === 'drilldown' ? [] : effectiveBlockHints,
|
|
340
|
+
excludedArtifactIds,
|
|
341
|
+
kg,
|
|
342
|
+
}) : null;
|
|
343
|
+
let artifactHit = drilldownCertifiedHit ?? unsafeCatalogCertifiedHit
|
|
344
|
+
?? (catalogCertifiedHit ? null : fallbackCertifiedHit);
|
|
345
|
+
// Certified remains first when it actually covers the question. If the
|
|
346
|
+
// retrieved block does not fit but a governed semantic metric does, never
|
|
347
|
+
// let the broad catalog match pre-empt Lane 2.
|
|
300
348
|
// A certified TERM / BUSINESS VIEW is documentation, not data: it can be the
|
|
301
349
|
// terminal answer only for a definition-style question with no data ask. A
|
|
302
350
|
// question that requests a data shape (dimensions/measures/outputs/top-N) or
|
|
@@ -429,8 +477,6 @@ async function runAnswerLoop(input) {
|
|
|
429
477
|
// a governed metric can answer. Match by name + synonyms + measure family + hybrid
|
|
430
478
|
// rank over the FTS semantic hits, then ALL metric KG nodes (revenue ⇄
|
|
431
479
|
// cumulative_revenue). Certified-first is still preserved (checked above).
|
|
432
|
-
const semanticMetricNodes = collectMetricCandidates(semanticHits, considered, kg);
|
|
433
|
-
let semanticMetricMatch = await matchSemanticMetric(question, semanticMetricNodes).catch(() => null);
|
|
434
480
|
const clarifyBeforeGeneration = shouldClarifyBeforeGeneration({
|
|
435
481
|
intent,
|
|
436
482
|
routeDecision: catalogRoute,
|
|
@@ -506,9 +552,27 @@ async function runAnswerLoop(input) {
|
|
|
506
552
|
const matchedMetricHit = semanticMetricMatch
|
|
507
553
|
? [{ node: semanticMetricMatch.metric, score: Math.max(semanticMetricMatch.score, CERTIFIED_HIT_THRESHOLD) }]
|
|
508
554
|
: [];
|
|
555
|
+
// Reserve prompt slots per source instead of appending semantic hits LAST and
|
|
556
|
+
// truncating the whole list to 14 — the old behavior dropped semantic-model hits
|
|
557
|
+
// out of the generation prompt entirely whenever artifact/business hits filled
|
|
558
|
+
// the budget first, so even a question a semantic metric could answer never saw
|
|
559
|
+
// the metric. Each group gets a guaranteed minimum; leftover budget is filled
|
|
560
|
+
// round-robin. See interleaveContextHits.
|
|
509
561
|
const contextHits = activeTier === 'semantic_layer'
|
|
510
|
-
? [
|
|
511
|
-
|
|
562
|
+
? interleaveContextHits([
|
|
563
|
+
{ hits: matchedMetricHit, reserve: matchedMetricHit.length },
|
|
564
|
+
{ hits: semanticHits, reserve: 5 },
|
|
565
|
+
{ hits: trustedArtifactContext, reserve: 3 },
|
|
566
|
+
{ hits: reviewRequiredArtifactHits, reserve: 2 },
|
|
567
|
+
{ hits: businessHits.slice(0, 4), reserve: 2 },
|
|
568
|
+
{ hits: manifestHits, reserve: 2 },
|
|
569
|
+
], 14)
|
|
570
|
+
: interleaveContextHits([
|
|
571
|
+
{ hits: trustedArtifactContext, reserve: 5 },
|
|
572
|
+
{ hits: reviewRequiredArtifactHits, reserve: 3 },
|
|
573
|
+
{ hits: businessHits.slice(0, 4), reserve: 3 },
|
|
574
|
+
{ hits: manifestHits, reserve: 3 },
|
|
575
|
+
], 14);
|
|
512
576
|
const contextNodes = mergeNodes(followUpSourceBlock && input.followUp?.kind === 'drilldown' ? [followUpSourceBlock] : [], (contextHits.length > 0 ? contextHits : considered.slice(0, 6)).map((h) => h.node));
|
|
513
577
|
const kgJoinPathHints = buildKgJoinPathHints(kg, contextNodes, questionPlan);
|
|
514
578
|
const contextBlocks = contextNodes.filter((n) => n.kind === 'block');
|
|
@@ -527,7 +591,7 @@ async function runAnswerLoop(input) {
|
|
|
527
591
|
messages.push({ role: 'system', content: skillsPrompt });
|
|
528
592
|
messages.push({
|
|
529
593
|
role: 'system',
|
|
530
|
-
content: renderContextPrompt(contextBlocks, contextBusiness, contextOther, activeTier, input.memoryContext ?? [], input.extraContext, input.followUp, schemaContext, intent, input.contextPack, input.conversationSnapshot, kgJoinPathHints, promptBudget),
|
|
594
|
+
content: renderContextPrompt(contextBlocks, contextBusiness, contextOther, activeTier, input.memoryContext ?? [], input.extraContext, input.followUp, schemaContext, intent, input.contextPack, input.conversationSnapshot, kgJoinPathHints, promptBudget, input.semanticDriver),
|
|
531
595
|
});
|
|
532
596
|
messages.push({ role: 'user', content: question });
|
|
533
597
|
// ── Tier 2: semantic-layer metrics + dimensions (governed hierarchy) ──────
|
|
@@ -611,14 +675,55 @@ async function runAnswerLoop(input) {
|
|
|
611
675
|
contextPack: input.contextPack,
|
|
612
676
|
schemaContext,
|
|
613
677
|
});
|
|
678
|
+
// W2.2 — certified-block adaptation lane. When a certified block is context-only
|
|
679
|
+
// ONLY because the question adds exactly one filter whose value maps to a column
|
|
680
|
+
// the block already outputs, adapt the certified SQL (wrap + filter its result)
|
|
681
|
+
// instead of regenerating from scratch. It executes through the governed path
|
|
682
|
+
// (pre-validated, since the wrapper only restricts an already-certified result)
|
|
683
|
+
// and is labeled BELOW certified. Falls through to generation on any miss.
|
|
684
|
+
let certifiedAdaptation;
|
|
685
|
+
if (!semanticBridgeAnswer && !metricFirst && input.executeGeneratedSql) {
|
|
686
|
+
const fit = input.contextPack?.routeDecision?.blockFit;
|
|
687
|
+
const sourceBlock = input.contextPack?.allowedSqlContext?.sourceBlockSql?.[0];
|
|
688
|
+
if (fit && sourceBlock?.sql) {
|
|
689
|
+
const shape = extractSimpleSelectShape(sourceBlock.sql);
|
|
690
|
+
const blockOutputs = shape
|
|
691
|
+
? shape.selectExpressions.map((expression) => selectExpressionOutputName(expression)).filter((name) => Boolean(name))
|
|
692
|
+
: [];
|
|
693
|
+
certifiedAdaptation = planCertifiedAdaptation({
|
|
694
|
+
blockFit: fit,
|
|
695
|
+
certifiedSql: sourceBlock.sql,
|
|
696
|
+
blockName: sourceBlock.name,
|
|
697
|
+
blockOutputs,
|
|
698
|
+
resolveFilterColumn: (value) => resolveFilterValueColumns(value, schemaContext),
|
|
699
|
+
}) ?? undefined;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
614
702
|
let proposed = '';
|
|
615
703
|
let parsed;
|
|
616
704
|
const proposalToolCalls = [...semanticBridgeToolCalls];
|
|
705
|
+
// Stage-B toolset: the host's warehouse/validation tools PLUS the answer loop's
|
|
706
|
+
// own governed semantic tools (search_semantic_layer, compile_semantic_query,
|
|
707
|
+
// scan_manifest), so tool-driven generation can compile governed SQL itself and
|
|
708
|
+
// grep the live graph — across every provider (native or text-protocol). Every
|
|
709
|
+
// governed compile is recorded so the answer can be labeled governed, not
|
|
710
|
+
// hand-written, downstream (deriveAgenticTrust).
|
|
711
|
+
const compiledSemanticRecords = [];
|
|
712
|
+
const stageBTools = [
|
|
713
|
+
...(input.answerLoopTools ?? []),
|
|
714
|
+
...buildSemanticStageTools({
|
|
715
|
+
semanticLayer: input.semanticLayer,
|
|
716
|
+
kg,
|
|
717
|
+
driver: input.semanticDriver,
|
|
718
|
+
tableMapping: input.semanticTableMapping,
|
|
719
|
+
onCompiled: (record) => compiledSemanticRecords.push(record),
|
|
720
|
+
}),
|
|
721
|
+
];
|
|
617
722
|
if (semanticBridgeAnswer) {
|
|
618
723
|
governedMetricAnswer = true;
|
|
619
724
|
parsed = {
|
|
620
725
|
sql: semanticBridgeAnswer.sql,
|
|
621
|
-
text: `Answered from governed semantic metric${semanticBridgeAnswer.metrics.length === 1 ? '' : 's'} ${semanticBridgeAnswer.metrics.join(', ')}${semanticBridgeAnswer.dimensions.length > 0 ? ` by ${semanticBridgeAnswer.dimensions.join(', ')}` : ''}.
|
|
726
|
+
text: `Answered from governed semantic metric${semanticBridgeAnswer.metrics.length === 1 ? '' : 's'} ${semanticBridgeAnswer.metrics.join(', ')}${semanticBridgeAnswer.dimensions.length > 0 ? ` by ${semanticBridgeAnswer.dimensions.join(', ')}` : ''}. The semantic compiler owns this query; saving it as a reusable certified block still requires review.`,
|
|
622
727
|
viz: semanticBridgeAnswer.dimensions.length === 0 && !semanticBridgeAnswer.timeDimension ? 'single_value' : undefined,
|
|
623
728
|
};
|
|
624
729
|
}
|
|
@@ -627,16 +732,39 @@ async function runAnswerLoop(input) {
|
|
|
627
732
|
governedMetricAnswer = true;
|
|
628
733
|
parsed = {
|
|
629
734
|
sql: metricFirst.sql,
|
|
630
|
-
text: `Answered from the governed metric ${metricFirst.metric.name}${metricFirst.dimensions.length > 0 ? ` by ${metricFirst.dimensions.join(', ')}` : ''}.
|
|
735
|
+
text: `Answered from the governed metric ${metricFirst.metric.name}${metricFirst.dimensions.length > 0 ? ` by ${metricFirst.dimensions.join(', ')}` : ''}. The semantic definition owns the calculation; reusable block certification remains a separate review.`,
|
|
631
736
|
viz: metricFirst.dimensions.length === 0 ? 'single_value' : undefined,
|
|
632
737
|
};
|
|
633
738
|
}
|
|
739
|
+
else if (certifiedAdaptation) {
|
|
740
|
+
// Pre-validated governed path (the wrapper only restricts a certified result).
|
|
741
|
+
governedMetricAnswer = true;
|
|
742
|
+
parsed = {
|
|
743
|
+
sql: certifiedAdaptation.sql,
|
|
744
|
+
text: certifiedAdaptation.provenance,
|
|
745
|
+
};
|
|
746
|
+
}
|
|
634
747
|
else {
|
|
748
|
+
// Tier 2.5 — METRIC-ANCHORED generation. A governed metric matched this question,
|
|
749
|
+
// but its semantic definition couldn't compose the exact requested shape (typically
|
|
750
|
+
// the breakdown needs a join the semantic layer doesn't own — e.g. a location-grain
|
|
751
|
+
// tax metric asked for "by product"). Rather than throw the metric away and let the
|
|
752
|
+
// model reinvent the aggregation from scratch, inject the metric's CERTIFIED
|
|
753
|
+
// definition as a required building block, so the generated SQL reuses the trusted
|
|
754
|
+
// measure and only generates the join/grouping around it. Keeps the number
|
|
755
|
+
// consistent with the governed metric. Degrades to plain generation when the metric
|
|
756
|
+
// has no resolvable definition. The grain ledger + validation still gate the output.
|
|
757
|
+
const metricAnchor = semanticMetricMatch && input.semanticLayer
|
|
758
|
+
? resolveGovernedMetricDefinition(semanticMetricMatch.metric, semanticMetricNodes, input.semanticLayer)
|
|
759
|
+
: undefined;
|
|
760
|
+
const generationMessages = metricAnchor
|
|
761
|
+
? [...messages, { role: 'system', content: metricAnchorInstruction(metricAnchor.metric.name, metricAnchor.def) }]
|
|
762
|
+
: messages;
|
|
635
763
|
try {
|
|
636
764
|
proposed = await generateProposalWithOptionalTools({
|
|
637
765
|
provider,
|
|
638
|
-
messages,
|
|
639
|
-
tools:
|
|
766
|
+
messages: generationMessages,
|
|
767
|
+
tools: stageBTools,
|
|
640
768
|
questionPlan,
|
|
641
769
|
intent,
|
|
642
770
|
signal: input.signal,
|
|
@@ -676,6 +804,35 @@ async function runAnswerLoop(input) {
|
|
|
676
804
|
};
|
|
677
805
|
}
|
|
678
806
|
parsed = parseProposal(proposed);
|
|
807
|
+
// Surface the tier choice (WS3): when generation was anchored on a governed
|
|
808
|
+
// metric, say so, so the answer reads as "reused the certified measure" rather
|
|
809
|
+
// than an opaque hand-written query.
|
|
810
|
+
if (metricAnchor && parsed.sql) {
|
|
811
|
+
const anchorNote = `Computed using the governed metric ${metricAnchor.metric.name}'s certified definition, joined to add the requested breakdown. Review-required (not itself certified).`;
|
|
812
|
+
parsed = { ...parsed, text: parsed.text ? `${parsed.text}\n\n${anchorNote}` : anchorNote };
|
|
813
|
+
}
|
|
814
|
+
// Stage-B governed promotion: when the model drove compile_semantic_query and
|
|
815
|
+
// adopted its output VERBATIM (deriveAgenticTrust — exact match only), the SQL
|
|
816
|
+
// is governed semantic SQL the compiler owns and already validated, not
|
|
817
|
+
// hand-written. Treat it like the deterministic governed path: skip the
|
|
818
|
+
// hallucination guard (governedMetricAnswer) and note the provenance. Any edit
|
|
819
|
+
// to the compiled SQL falls back to generated/review-required by construction.
|
|
820
|
+
if (!governedMetricAnswer && parsed.sql) {
|
|
821
|
+
const trust = deriveAgenticTrust(parsed.sql, compiledSemanticRecords);
|
|
822
|
+
if (trust.tier === 'semantic_metric' && trust.compiled) {
|
|
823
|
+
governedMetricAnswer = true;
|
|
824
|
+
const compiled = trust.compiled;
|
|
825
|
+
const governedNote = `Answered from governed semantic metric${compiled.metrics.length === 1 ? '' : 's'} ${compiled.metrics.join(', ')}${compiled.dimensions.length > 0 ? ` by ${compiled.dimensions.join(', ')}` : ''} (compiled via the semantic layer). Reusable block certification remains a separate review.`;
|
|
826
|
+
parsed = { ...parsed, text: parsed.text ? `${parsed.text}\n\n${governedNote}` : governedNote };
|
|
827
|
+
proposalToolCalls.push({
|
|
828
|
+
name: 'compile_semantic_query',
|
|
829
|
+
status: 'checked',
|
|
830
|
+
inputSummary: `metrics: ${compiled.metrics.join(', ')}${compiled.dimensions.length ? `; by ${compiled.dimensions.join(', ')}` : ''}`,
|
|
831
|
+
outputSummary: 'Model-selected semantic members compiled to governed SQL',
|
|
832
|
+
order: proposalToolCalls.length + 1,
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
}
|
|
679
836
|
}
|
|
680
837
|
let deepCandidateResult;
|
|
681
838
|
let deepCandidateExecutionError;
|
|
@@ -692,6 +849,7 @@ async function runAnswerLoop(input) {
|
|
|
692
849
|
executeGeneratedSql: input.executeGeneratedSql,
|
|
693
850
|
signal: input.signal,
|
|
694
851
|
reasoningEffort: input.reasoningEffort,
|
|
852
|
+
maxAlternatives: deepAlternativeCountForQuestion(questionPlan, intent),
|
|
695
853
|
});
|
|
696
854
|
if (selection.selected) {
|
|
697
855
|
proposed = selection.selected.raw;
|
|
@@ -725,7 +883,7 @@ async function runAnswerLoop(input) {
|
|
|
725
883
|
parsed = {
|
|
726
884
|
sql: resolved.sql,
|
|
727
885
|
text: parsed.text ||
|
|
728
|
-
`Answered from the governed metric ${resolved.metric.name}.
|
|
886
|
+
`Answered from the governed metric ${resolved.metric.name}. The semantic definition owns the calculation; reusable block certification remains a separate review.`,
|
|
729
887
|
viz: parsed.viz ?? 'single_value',
|
|
730
888
|
};
|
|
731
889
|
}
|
|
@@ -751,7 +909,7 @@ async function runAnswerLoop(input) {
|
|
|
751
909
|
proposed = await generateProposalWithOptionalTools({
|
|
752
910
|
provider,
|
|
753
911
|
messages: [...messages, { role: 'system', content: FORCE_JOIN_INSTRUCTION }],
|
|
754
|
-
tools:
|
|
912
|
+
tools: stageBTools,
|
|
755
913
|
questionPlan,
|
|
756
914
|
intent,
|
|
757
915
|
signal: input.signal,
|
|
@@ -959,6 +1117,21 @@ async function runAnswerLoop(input) {
|
|
|
959
1117
|
// Repair is best-effort — the refusal below stays the honest fallback.
|
|
960
1118
|
}
|
|
961
1119
|
}
|
|
1120
|
+
// W1.3 — deterministic, warn-only fan-out check. When the generated SQL aggregates
|
|
1121
|
+
// an additive measure across a one-to-many join (grain ledger knows the keys), the
|
|
1122
|
+
// number can silently double-count. Surface a caution so it is reviewed. This never
|
|
1123
|
+
// blocks: it only appends to warnings, and it is conservative (no key data ⇒ no flag).
|
|
1124
|
+
if (contextValidation.ok && parsed.sql && input.contextPack?.objects?.length) {
|
|
1125
|
+
try {
|
|
1126
|
+
const fanoutWarnings = fanoutWarningsForSql(parsed.sql, input.contextPack.objects);
|
|
1127
|
+
if (fanoutWarnings.length > 0) {
|
|
1128
|
+
contextValidation = { ok: true, warnings: [...contextValidation.warnings, ...fanoutWarnings] };
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
catch {
|
|
1132
|
+
// Fan-out detection is advisory; any failure must not affect the answer.
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
962
1135
|
if (!contextValidation.ok) {
|
|
963
1136
|
const text = `I could not safely prepare this review-required DQL artifact from the inspected context. The SQL preview failed validation: ${contextValidation.error}`;
|
|
964
1137
|
const analysisPlan = buildAnalysisPlan({
|
|
@@ -1239,26 +1412,32 @@ async function runAnswerLoop(input) {
|
|
|
1239
1412
|
? { ...dqlArtifact, sourcePath: dqlArtifact.sourcePath ?? draftBlock.path }
|
|
1240
1413
|
: dqlArtifact;
|
|
1241
1414
|
const sourceCertifiedBlock = followUpSourceBlock?.name ?? input.followUp?.sourceBlockName;
|
|
1242
|
-
const trustExplanation =
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1415
|
+
const trustExplanation = governedMetricAnswer
|
|
1416
|
+
? undefined
|
|
1417
|
+
: generatedTrustExplanation({
|
|
1418
|
+
followUp: input.followUp,
|
|
1419
|
+
sourceCertifiedBlock,
|
|
1420
|
+
draftBlock,
|
|
1421
|
+
});
|
|
1247
1422
|
const cleanedSummary = cleanGeneratedSummary(parsed.text);
|
|
1248
1423
|
const generatedText = [partialShapeWarning, trustExplanation, cleanedSummary]
|
|
1249
1424
|
.filter(Boolean)
|
|
1250
1425
|
.join('\n\n');
|
|
1251
|
-
|
|
1426
|
+
// A metric can be resolved directly from the loaded semantic layer even if
|
|
1427
|
+
// the small metadata pack happened to contain only dbt objects. The answer
|
|
1428
|
+
// is still compiler-owned semantic SQL in that case; do not accidentally
|
|
1429
|
+
// downgrade its route/trust to generated SQL because of retrieval order.
|
|
1430
|
+
const semanticMetricCertification = governedMetricAnswer
|
|
1252
1431
|
? semanticMetricMatch?.metric.certification
|
|
1253
1432
|
: undefined;
|
|
1254
1433
|
const certifiedMetricAnswer = semanticMetricCertification === 'certified' || semanticMetricCertification === 'reviewed';
|
|
1255
1434
|
return {
|
|
1256
1435
|
kind: 'uncertified',
|
|
1257
|
-
sourceTier: activeTier,
|
|
1258
|
-
certification: 'ai_generated',
|
|
1259
|
-
reviewStatus: 'draft_ready',
|
|
1436
|
+
sourceTier: governedMetricAnswer ? 'semantic_layer' : activeTier,
|
|
1437
|
+
certification: governedMetricAnswer ? 'governed' : 'ai_generated',
|
|
1438
|
+
reviewStatus: governedMetricAnswer ? 'governed' : 'draft_ready',
|
|
1260
1439
|
semanticMetricCertification,
|
|
1261
|
-
confidence: certifiedMetricAnswer ? 0.8 : governedMetricAnswer
|
|
1440
|
+
confidence: certifiedMetricAnswer ? 0.8 : governedMetricAnswer ? 0.72 : 0.55,
|
|
1262
1441
|
text: generatedText,
|
|
1263
1442
|
answer: generatedText,
|
|
1264
1443
|
proposedSql: parsed.sql,
|
|
@@ -1308,7 +1487,7 @@ async function runAnswerLoop(input) {
|
|
|
1308
1487
|
providerUsed: provider.name,
|
|
1309
1488
|
// Carry the governed metric match so the exit point can name a
|
|
1310
1489
|
// `semantic_metric` route (spec 17, part C).
|
|
1311
|
-
_semanticMetricMatch: governedMetricAnswer
|
|
1490
|
+
_semanticMetricMatch: governedMetricAnswer ? semanticMetricMatch ?? undefined : undefined,
|
|
1312
1491
|
};
|
|
1313
1492
|
}
|
|
1314
1493
|
function trimResultToRequestedTopN(result, plan) {
|
|
@@ -1353,13 +1532,21 @@ Rules:
|
|
|
1353
1532
|
separate SQL code fence when using JSON. The optional "dql" object is
|
|
1354
1533
|
metadata for the deterministic draft-DQL renderer; do not hand-write a full
|
|
1355
1534
|
DQL block.
|
|
1535
|
+
Design the SELECT for a business reader: prefer a joined/display name, label,
|
|
1536
|
+
title, or business value over raw *_id, *_uuid, *_key, or technical codes.
|
|
1537
|
+
Alias calculated outputs with clear business names. Include identifiers only
|
|
1538
|
+
when the question asks for them or no readable field exists.
|
|
1356
1539
|
4. In summary, state your QUERY PLAN first: the grain (one row per WHAT), the
|
|
1357
1540
|
measures and how they aggregate, the dimensions/filters, and the exact join
|
|
1358
1541
|
path + join keys between the grounded tables. Then make the SQL match that
|
|
1359
1542
|
plan — an explicit grain and join path prevents wrong-grain answers and
|
|
1360
1543
|
fan-out (row-multiplying) joins.
|
|
1361
|
-
5.
|
|
1362
|
-
|
|
1544
|
+
5. Choose a visualization deliberately in the "viz" JSON field: use single_value
|
|
1545
|
+
or kpi for one aggregate, line/area only for an ordered time series, bar for a
|
|
1546
|
+
categorical comparison, grouped-bar for multiple measures by one category,
|
|
1547
|
+
scatter for two continuous measures, and table when the result is not chartable.
|
|
1548
|
+
Do not default to bar. The runtime validates this preference against returned
|
|
1549
|
+
rows before displaying it.
|
|
1363
1550
|
6. NEVER fabricate column names that are not present in the supplied schema,
|
|
1364
1551
|
dbt metadata, or certified source SQL shape context. If a requested filter
|
|
1365
1552
|
value is supplied as a matched value, prefer the table and column that
|
|
@@ -1368,15 +1555,19 @@ Rules:
|
|
|
1368
1555
|
Do NOT use dbt/Jinja macros such as {{ ref(...) }} or {{ source(...) }} in
|
|
1369
1556
|
proposed SQL. Do not emit INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, COPY,
|
|
1370
1557
|
PRAGMA, SET, or multiple statements.
|
|
1371
|
-
8. If the schema is insufficient
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1558
|
+
8. If the schema is insufficient, DISCOVER before you decline: use
|
|
1559
|
+
search_metadata to find candidate relations, get_table_schema to confirm a
|
|
1560
|
+
relation's real columns and inferred join keys, and expand_context for a
|
|
1561
|
+
relation you already know by name. Only ask a clarifying question once those
|
|
1562
|
+
come up empty AND the question is genuinely ambiguous — never decline just
|
|
1563
|
+
because the initially supplied context pack didn't already include the table.
|
|
1564
|
+
A MULTI-ENTITY question ("top customers who bought the top products", "accounts
|
|
1565
|
+
with the most overdue invoices") is NOT insufficient context when the grounded
|
|
1566
|
+
tables and a join route between them are supplied (see any "Knowledge graph
|
|
1567
|
+
join routes" section) or discoverable via the tools above. In that case you
|
|
1568
|
+
MUST compose the joined SELECT that answers it directly — never refuse it or
|
|
1569
|
+
offer to show the parts as separate datasets. Reserve the clarifying question
|
|
1570
|
+
for a genuinely absent table, column, or join key.
|
|
1380
1571
|
9. Write directly to the analyst. Do not say "the user is asking", "the user
|
|
1381
1572
|
requested", "I will generate", or describe internal routing. State the
|
|
1382
1573
|
answer, the certified context used, the DQL artifact expectation, and the
|
|
@@ -1390,7 +1581,20 @@ Rules:
|
|
|
1390
1581
|
// combined dataset — show them separately" refusal into the join the user asked
|
|
1391
1582
|
// for, while still allowing an honest refusal if context is truly missing.
|
|
1392
1583
|
const FORCE_JOIN_INSTRUCTION = `Your previous attempt declined to produce SQL. Re-read the supplied schema, metadata, and any "Knowledge graph join routes": this question CAN be answered by joining the grounded tables along their documented keys. Do NOT refuse, and do NOT suggest showing the datasets separately — compose ONE read-only SELECT/WITH that joins the relevant tables to answer it directly, following the JSON contract from rule 3. State the grain and the exact join path in the summary. Only if a required table, column, or join key is truly absent from the supplied context may you still ask a clarifying question.`;
|
|
1393
|
-
|
|
1584
|
+
// Metric-anchored generation (Tier 2.5): a governed metric matched the question but
|
|
1585
|
+
// the semantic layer couldn't compose the exact shape. Reuse the metric's CERTIFIED
|
|
1586
|
+
// definition as the measure rather than reinventing it, so the number stays consistent
|
|
1587
|
+
// with the governed metric — only the join/grouping around it is generated.
|
|
1588
|
+
function metricAnchorInstruction(metricName, def) {
|
|
1589
|
+
return `A GOVERNED metric matched this question, but the semantic layer could not compose the exact requested shape on its own (the breakdown likely needs a join the metric's table does not own). You MUST compute the measure using this certified definition — do NOT redefine, rename, or approximate it:\n metric "${metricName}": ${def.expr} (defined over ${def.table})\nCompose ONE read-only SELECT/WITH that computes exactly this expression as the measure, joining ${def.table} to the other grounded tables along their documented keys to add the requested dimensions, then GROUP BY those dimensions. State the join path and grain in the summary. Reusing the governed definition keeps the answer consistent with the certified metric; only fall back to redefining the measure if ${def.table} genuinely cannot be joined to the requested breakdown.`;
|
|
1590
|
+
}
|
|
1591
|
+
function renderContextPrompt(blocks, businessContext, others, activeTier, memoryContext, extraContext, followUp, schemaContext = [], intent = 'ad_hoc_analysis', contextPack, conversationSnapshot, kgJoinPathHints = [], budget = QUICK_PROMPT_CONTEXT_BUDGET, driver) {
|
|
1592
|
+
// W1.5 — dialect conventions so the model writes warehouse-correct SQL
|
|
1593
|
+
// (quoting, row-limiting, date functions) instead of a DuckDB/Postgres default
|
|
1594
|
+
// that fails on Snowflake/BigQuery/etc. Only emitted when a driver is known.
|
|
1595
|
+
const dialectSection = driver
|
|
1596
|
+
? `\n\n## SQL dialect\n\n${describeDialectForPrompt(driver)}`
|
|
1597
|
+
: '';
|
|
1394
1598
|
const intentSection = `## Routing intent\n\nintent: ${intent}\n${intent === 'exact_certified_lookup'
|
|
1395
1599
|
? 'Use a certified artifact only if it exactly answers the question.'
|
|
1396
1600
|
: 'Prepare a review-required DQL artifact with a SQL preview for this question. Certified blocks are trusted context, not a reason to answer the wrong grain.'}`;
|
|
@@ -1459,7 +1663,7 @@ function renderContextPrompt(blocks, businessContext, others, activeTier, memory
|
|
|
1459
1663
|
const contextPackSection = contextPack
|
|
1460
1664
|
? `\n\n## Local metadata context pack\n\n${renderContextPackForPrompt(contextPack, budget)}`
|
|
1461
1665
|
: '';
|
|
1462
|
-
return `${intentSection}${budgetSection}\n\n${blockSection}${businessSection}${otherSection}${kgJoinSection}${schemaSection}${contextPackSection}${memorySection}${hintsSection}${conversationSection}${extraSection}${followUpSection}`;
|
|
1666
|
+
return `${intentSection}${budgetSection}${dialectSection}\n\n${blockSection}${businessSection}${otherSection}${kgJoinSection}${schemaSection}${contextPackSection}${memorySection}${hintsSection}${conversationSection}${extraSection}${followUpSection}`;
|
|
1463
1667
|
}
|
|
1464
1668
|
function buildKgJoinPathHints(kg, nodes, questionPlan) {
|
|
1465
1669
|
const candidates = nodes
|
|
@@ -2968,25 +3172,29 @@ async function generateProposalWithOptionalTools(input) {
|
|
|
2968
3172
|
signal: input.signal,
|
|
2969
3173
|
reasoningEffort: input.reasoningEffort,
|
|
2970
3174
|
};
|
|
2971
|
-
|
|
3175
|
+
// No tools → plain generation (nothing for the loop to drive).
|
|
3176
|
+
if (tools.length === 0) {
|
|
2972
3177
|
return input.provider.generate(input.messages, options);
|
|
2973
3178
|
}
|
|
2974
3179
|
const toolBudget = proposalToolBudgetForQuestion(input.questionPlan, input.intent, {
|
|
2975
3180
|
analysisDepth: input.analysisDepth,
|
|
2976
3181
|
reasoningEffort: input.reasoningEffort,
|
|
2977
3182
|
});
|
|
2978
|
-
const toolPolicy =
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
3183
|
+
const toolPolicy = [
|
|
3184
|
+
'You may use the supplied DQL tools to inspect semantic members, certified context, metadata context, and bounded repair options.',
|
|
3185
|
+
`Tool budget for this question: ${toolBudget.maxToolCalls} call(s) (${toolBudget.effortClass}: ${toolBudget.reason}). Stop as soon as a lane can answer.`,
|
|
3186
|
+
'Prefer a governed semantic compile (search_semantic_layer → compile_semantic_query) over hand-written SQL when the semantic layer contains the requested metric/dimensions/time grain.',
|
|
3187
|
+
'When the supplied context is missing a table, column, or join key, DISCOVER it with `search_metadata` (find candidate relations) then `get_table_schema` (confirm real columns + inferred join keys) before declining. Use `search_project_files` for a bounded live source grep when indexed retrieval missed an identifier; use `scan_manifest` for cached graph objects; do not loop on the same failed context.',
|
|
3188
|
+
'When unsure a relation/column exists, validate a composed query with `validate_sql` rather than guessing.',
|
|
3189
|
+
'Final response must be a single ```json fenced object with summary, sql, viz, outputs, and optional dql metadata fields.',
|
|
3190
|
+
].join('\n');
|
|
3191
|
+
// runAgenticToolLoop drives the loop over ANY provider: native tool use where the
|
|
3192
|
+
// provider implements generateWithTools (Claude/OpenAI), and an equivalent text
|
|
3193
|
+
// protocol otherwise (subscription-CLI passthrough, Ollama). This is what gives
|
|
3194
|
+
// every provider — not just the two API ones — a real tool-driven Stage B.
|
|
3195
|
+
return runAgenticToolLoop(input.provider, [...input.messages], tools, {
|
|
2989
3196
|
...options,
|
|
3197
|
+
toolPolicy,
|
|
2990
3198
|
maxToolCalls: toolBudget.maxToolCalls,
|
|
2991
3199
|
onToolCall: (event) => {
|
|
2992
3200
|
const sink = input.toolCalls;
|
|
@@ -3130,8 +3338,14 @@ async function generateDeepAlternativeProposals(input) {
|
|
|
3130
3338
|
'Create a fourth candidate by DECOMPOSITION: break the question into sub-questions, solve each as a CTE, then compose the final SELECT — avoiding assumptions hidden in the first candidate.',
|
|
3131
3339
|
];
|
|
3132
3340
|
const temperatures = [0.2, 0.35, 0.5];
|
|
3133
|
-
|
|
3134
|
-
for
|
|
3341
|
+
// How many diverse alternatives to generate is set by the question SHAPE (S1):
|
|
3342
|
+
// a lightweight 1-candidate agreement check for join/breakdown shapes, the full
|
|
3343
|
+
// 3-candidate vote for deep-research, none for a single-table lookup. The
|
|
3344
|
+
// generations are independent, so they run in PARALLEL — the deep vote's
|
|
3345
|
+
// wall-clock cost is one generation, not the serial sum of all of them.
|
|
3346
|
+
const count = Math.max(0, Math.min(input.maxAlternatives ?? variants.length, variants.length));
|
|
3347
|
+
const selected = variants.slice(0, count);
|
|
3348
|
+
const results = await Promise.all(selected.map(async (instruction, index) => {
|
|
3135
3349
|
try {
|
|
3136
3350
|
const raw = await input.provider.generate([
|
|
3137
3351
|
...input.messages,
|
|
@@ -3151,14 +3365,14 @@ async function generateDeepAlternativeProposals(input) {
|
|
|
3151
3365
|
reasoningEffort: input.reasoningEffort,
|
|
3152
3366
|
temperature: temperatures[index] ?? 0.4,
|
|
3153
3367
|
});
|
|
3154
|
-
|
|
3155
|
-
out.push(raw);
|
|
3368
|
+
return raw.trim() ? raw : '';
|
|
3156
3369
|
}
|
|
3157
3370
|
catch {
|
|
3158
3371
|
// Alternative candidates are opportunistic; the initial candidate remains.
|
|
3372
|
+
return '';
|
|
3159
3373
|
}
|
|
3160
|
-
}
|
|
3161
|
-
return
|
|
3374
|
+
}));
|
|
3375
|
+
return results.filter((raw) => raw.trim());
|
|
3162
3376
|
}
|
|
3163
3377
|
function cloneParsedProposal(proposal) {
|
|
3164
3378
|
return {
|
|
@@ -3190,6 +3404,7 @@ function evidenceToolCallFromEvent(event, order) {
|
|
|
3190
3404
|
inputSummary: summarizeEvidencePayload(event.input),
|
|
3191
3405
|
outputSummary: summarizeEvidencePayload(event.output),
|
|
3192
3406
|
order,
|
|
3407
|
+
...(typeof event.durationMs === 'number' ? { durationMs: event.durationMs } : {}),
|
|
3193
3408
|
};
|
|
3194
3409
|
}
|
|
3195
3410
|
function summarizeEvidencePayload(value, maxLength = 700) {
|
|
@@ -3295,6 +3510,40 @@ function certifiedFreshnessCaveat(artifact) {
|
|
|
3295
3510
|
return undefined;
|
|
3296
3511
|
}
|
|
3297
3512
|
}
|
|
3513
|
+
function provenanceSourceTierLabel(tier) {
|
|
3514
|
+
switch (tier) {
|
|
3515
|
+
case 'certified_artifact': return 'Certified block';
|
|
3516
|
+
case 'semantic_layer': return 'Governed semantic metric';
|
|
3517
|
+
case 'business_context': return 'Business context';
|
|
3518
|
+
case 'dbt_manifest': return 'Generated SQL';
|
|
3519
|
+
default: return undefined;
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
/**
|
|
3523
|
+
* Anthropic-style provenance footer: one line telling a stakeholder WHERE the
|
|
3524
|
+
* answer came from (source tier), how much to trust it, who owns the source, and
|
|
3525
|
+
* whether the underlying data is current. Built once at the single exit from the
|
|
3526
|
+
* finished answer; omitted for no-answer outcomes.
|
|
3527
|
+
*/
|
|
3528
|
+
export function buildProvenanceFooter(result, trust) {
|
|
3529
|
+
if (result.kind === 'no_answer')
|
|
3530
|
+
return undefined;
|
|
3531
|
+
const parts = [];
|
|
3532
|
+
const tierLabel = provenanceSourceTierLabel(result.sourceTier);
|
|
3533
|
+
if (tierLabel)
|
|
3534
|
+
parts.push(`Source: ${tierLabel}`);
|
|
3535
|
+
if (trust?.display)
|
|
3536
|
+
parts.push(`Trust: ${trust.display}`);
|
|
3537
|
+
if (result.block?.owner)
|
|
3538
|
+
parts.push(`Owner: ${result.block.owner}`);
|
|
3539
|
+
const freshness = result.block?.dataState === 'stale' ? 'stale — verify currency'
|
|
3540
|
+
: result.block?.dataState === 'failed' ? 'upstream run failed'
|
|
3541
|
+
: result.block?.dataState === 'fresh' ? 'current'
|
|
3542
|
+
: undefined;
|
|
3543
|
+
if (freshness)
|
|
3544
|
+
parts.push(`Data: ${freshness}`);
|
|
3545
|
+
return parts.length > 0 ? parts.join(' · ') : undefined;
|
|
3546
|
+
}
|
|
3298
3547
|
function mergeHits(...groups) {
|
|
3299
3548
|
const byId = new Map();
|
|
3300
3549
|
for (const group of groups) {
|
|
@@ -3316,6 +3565,53 @@ function mergeNodes(...groups) {
|
|
|
3316
3565
|
}
|
|
3317
3566
|
return Array.from(byId.values());
|
|
3318
3567
|
}
|
|
3568
|
+
/**
|
|
3569
|
+
* Merge prioritized hit groups into a single deduped list under a total budget,
|
|
3570
|
+
* giving each group a guaranteed minimum number of slots. Pass 1 takes up to
|
|
3571
|
+
* `reserve` from each group in priority order; pass 2 fills any remaining budget
|
|
3572
|
+
* round-robin from each group's leftovers. This prevents a high-priority group
|
|
3573
|
+
* from starving a lower one out of the prompt entirely (the semantic-hit
|
|
3574
|
+
* truncation bug) while still honoring priority for the reserved slots.
|
|
3575
|
+
*/
|
|
3576
|
+
function interleaveContextHits(groups, budget) {
|
|
3577
|
+
const seen = new Set();
|
|
3578
|
+
const out = [];
|
|
3579
|
+
const cursors = groups.map(() => 0);
|
|
3580
|
+
const take = (hit) => {
|
|
3581
|
+
if (out.length >= budget || seen.has(hit.node.nodeId))
|
|
3582
|
+
return false;
|
|
3583
|
+
seen.add(hit.node.nodeId);
|
|
3584
|
+
out.push(hit);
|
|
3585
|
+
return true;
|
|
3586
|
+
};
|
|
3587
|
+
// Pass 1: reserved minimums, in priority order.
|
|
3588
|
+
groups.forEach((group, gi) => {
|
|
3589
|
+
let taken = 0;
|
|
3590
|
+
while (cursors[gi] < group.hits.length && taken < group.reserve && out.length < budget) {
|
|
3591
|
+
if (take(group.hits[cursors[gi]]))
|
|
3592
|
+
taken += 1;
|
|
3593
|
+
cursors[gi] += 1;
|
|
3594
|
+
}
|
|
3595
|
+
});
|
|
3596
|
+
// Pass 2: round-robin fill from leftovers until the budget is exhausted.
|
|
3597
|
+
let progressed = true;
|
|
3598
|
+
while (out.length < budget && progressed) {
|
|
3599
|
+
progressed = false;
|
|
3600
|
+
for (let gi = 0; gi < groups.length && out.length < budget; gi += 1) {
|
|
3601
|
+
const group = groups[gi];
|
|
3602
|
+
while (cursors[gi] < group.hits.length) {
|
|
3603
|
+
const before = out.length;
|
|
3604
|
+
take(group.hits[cursors[gi]]);
|
|
3605
|
+
cursors[gi] += 1;
|
|
3606
|
+
if (out.length > before) {
|
|
3607
|
+
progressed = true;
|
|
3608
|
+
break;
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
return out;
|
|
3614
|
+
}
|
|
3319
3615
|
/**
|
|
3320
3616
|
* Candidate metric KG nodes for semantic-metric matching (spec 17, part C).
|
|
3321
3617
|
* Starts with the FTS semantic + considered hits, then folds in EVERY metric
|