@duckcodeailabs/dql-agent 1.6.4 → 1.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/answer-loop.d.ts +6 -1
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +357 -31
- package/dist/answer-loop.js.map +1 -1
- package/dist/app-builder.d.ts +31 -1
- package/dist/app-builder.d.ts.map +1 -1
- package/dist/app-builder.js +302 -72
- package/dist/app-builder.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/metadata/catalog.d.ts +280 -0
- package/dist/metadata/catalog.d.ts.map +1 -0
- package/dist/metadata/catalog.js +1991 -0
- package/dist/metadata/catalog.js.map +1 -0
- package/dist/providers/ollama.d.ts.map +1 -1
- package/dist/providers/ollama.js +2 -1
- package/dist/providers/ollama.js.map +1 -1
- package/package.json +4 -4
package/dist/answer-loop.d.ts
CHANGED
|
@@ -19,11 +19,12 @@ import type { KGNode, KGSearchHit } from './kg/types.js';
|
|
|
19
19
|
import type { AgentProvider } from './providers/types.js';
|
|
20
20
|
import type { Skill } from './skills/loader.js';
|
|
21
21
|
import type { AgentMemory } from './memory/sqlite-memory.js';
|
|
22
|
+
import type { LocalContextPack, MetadataAgentIntent } from './metadata/catalog.js';
|
|
22
23
|
export type AnswerKind = 'certified' | 'uncertified' | 'no_answer';
|
|
23
24
|
export type AnswerSourceTier = 'certified_artifact' | 'business_context' | 'semantic_layer' | 'dbt_manifest' | 'no_answer';
|
|
24
25
|
export type AnswerCertification = 'certified' | 'ai_generated' | 'analyst_review_required';
|
|
25
26
|
export type AnswerReviewStatus = 'none' | 'draft_ready' | 'analyst_review_required' | 'certified';
|
|
26
|
-
export type AgentIntent =
|
|
27
|
+
export type AgentIntent = MetadataAgentIntent | 'ad_hoc_analysis' | 'drillthrough';
|
|
27
28
|
export interface AgentCitation {
|
|
28
29
|
nodeId: string;
|
|
29
30
|
kind: KGNode['kind'] | 'memory' | 'runtime_schema';
|
|
@@ -170,6 +171,8 @@ export interface AgentAnswer {
|
|
|
170
171
|
analysisPlan?: AgentAnalysisPlan;
|
|
171
172
|
/** Provider name used (for telemetry / UI badge). */
|
|
172
173
|
providerUsed?: string;
|
|
174
|
+
/** Local SQLite metadata context pack used to ground retrieval, when supplied by the host. */
|
|
175
|
+
contextPack?: LocalContextPack;
|
|
173
176
|
/** Top KG hits the loop considered, useful for the UI's "we considered" panel. */
|
|
174
177
|
considered: KGSearchHit[];
|
|
175
178
|
}
|
|
@@ -228,6 +231,8 @@ export interface AnswerLoopInput {
|
|
|
228
231
|
executeGeneratedSql?: (sql: string) => Promise<AgentResultPayload>;
|
|
229
232
|
/** Runtime schema/column context supplied by the host for generated analysis. */
|
|
230
233
|
schemaContext?: AgentSchemaTable[];
|
|
234
|
+
/** Shared local metadata context pack from `.dql/cache/metadata.sqlite`. */
|
|
235
|
+
contextPack?: LocalContextPack;
|
|
231
236
|
}
|
|
232
237
|
export declare function answer(input: AnswerLoopInput): Promise<AgentAnswer>;
|
|
233
238
|
interface ParsedProposal {
|
|
@@ -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;
|
|
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
|
@@ -40,7 +40,8 @@ export async function answer(input) {
|
|
|
40
40
|
const semanticHits = kg.search({ query: question, domain, kinds: SEMANTIC_KINDS, limit: 12 });
|
|
41
41
|
const manifestHits = kg.search({ query: question, domain, kinds: MANIFEST_KINDS, limit: 12 });
|
|
42
42
|
const considered = mergeHits(artifactHits, semanticHits, manifestHits, kg.search({ query: question, domain, limit: 10 })).slice(0, 30);
|
|
43
|
-
const
|
|
43
|
+
const catalogRoute = input.contextPack?.routeDecision;
|
|
44
|
+
const fallbackIntent = classifyAgentIntent({
|
|
44
45
|
question,
|
|
45
46
|
followUp: input.followUp,
|
|
46
47
|
artifactHits,
|
|
@@ -48,18 +49,20 @@ export async function answer(input) {
|
|
|
48
49
|
manifestHits,
|
|
49
50
|
schemaContext: input.schemaContext ?? [],
|
|
50
51
|
});
|
|
52
|
+
const intent = catalogRoute ? agentIntentFromCatalogRoute(catalogRoute) : fallbackIntent;
|
|
51
53
|
// Stage 1: certified artifact match. Blocks can be executed; dashboards,
|
|
52
54
|
// Apps, and notebooks are returned as governed citations/navigation targets.
|
|
53
|
-
const artifactHit = intent
|
|
54
|
-
?
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
55
|
+
const artifactHit = shouldUseCertifiedRoute(catalogRoute, intent)
|
|
56
|
+
? certifiedHitFromContextPack(input.contextPack, kg)
|
|
57
|
+
?? pickCertifiedArtifact({
|
|
58
|
+
artifactHits,
|
|
59
|
+
executableArtifactHits,
|
|
60
|
+
businessHits,
|
|
61
|
+
question,
|
|
62
|
+
blockHints: input.followUp?.kind === 'drilldown' ? [] : effectiveBlockHints,
|
|
63
|
+
excludedArtifactIds,
|
|
64
|
+
kg,
|
|
65
|
+
})
|
|
63
66
|
: null;
|
|
64
67
|
if (artifactHit) {
|
|
65
68
|
let result;
|
|
@@ -89,7 +92,7 @@ export async function answer(input) {
|
|
|
89
92
|
const analysisPlan = buildAnalysisPlan({
|
|
90
93
|
question,
|
|
91
94
|
intent,
|
|
92
|
-
routeReason: 'The question matched a certified DQL artifact closely enough to answer without generating new SQL.',
|
|
95
|
+
routeReason: catalogRoute?.reason ?? 'The question matched a certified DQL artifact closely enough to answer without generating new SQL.',
|
|
93
96
|
selectedNodes: [artifactHit.node],
|
|
94
97
|
schemaContext: input.schemaContext ?? [],
|
|
95
98
|
sql: result?.sql,
|
|
@@ -124,19 +127,22 @@ export async function answer(input) {
|
|
|
124
127
|
memoryContext: input.memoryContext ?? [],
|
|
125
128
|
analysisPlan,
|
|
126
129
|
}),
|
|
130
|
+
contextPack: input.contextPack,
|
|
127
131
|
considered,
|
|
128
132
|
providerUsed: provider.name,
|
|
129
133
|
};
|
|
130
134
|
}
|
|
131
|
-
if (intent === 'clarify') {
|
|
132
|
-
const text = composeClarificationText(question, considered, input.schemaContext ?? []);
|
|
135
|
+
if (intent === 'clarify' || catalogRoute?.route === 'clarify') {
|
|
136
|
+
const text = composeCatalogClarificationText(question, catalogRoute) ?? composeClarificationText(question, considered, input.schemaContext ?? []);
|
|
133
137
|
const analysisPlan = buildAnalysisPlan({
|
|
134
138
|
question,
|
|
135
139
|
intent,
|
|
136
|
-
routeReason: 'No certified artifact, semantic object, dbt/source table, or runtime schema match was strong enough to safely generate SQL.',
|
|
140
|
+
routeReason: catalogRoute?.reason ?? 'No certified artifact, semantic object, dbt/source table, or runtime schema match was strong enough to safely generate SQL.',
|
|
137
141
|
selectedNodes: considered.slice(0, 4).map((hit) => hit.node),
|
|
138
142
|
schemaContext: input.schemaContext ?? [],
|
|
139
|
-
assumptions:
|
|
143
|
+
assumptions: catalogRoute?.missingContext.length
|
|
144
|
+
? catalogRoute.missingContext.map((item) => item.message)
|
|
145
|
+
: ['Need a clearer business object, measure, or grain before querying.'],
|
|
140
146
|
});
|
|
141
147
|
return {
|
|
142
148
|
kind: 'no_answer',
|
|
@@ -160,17 +166,18 @@ export async function answer(input) {
|
|
|
160
166
|
memoryContext: input.memoryContext ?? [],
|
|
161
167
|
analysisPlan,
|
|
162
168
|
}),
|
|
169
|
+
contextPack: input.contextPack,
|
|
163
170
|
considered,
|
|
164
171
|
providerUsed: provider.name,
|
|
165
172
|
};
|
|
166
173
|
}
|
|
167
174
|
// Stage 2/3: generate only after certified artifacts miss. Semantic context
|
|
168
175
|
// wins over raw dbt manifest context; memory is appended last as advisory.
|
|
169
|
-
const activeTier = semanticHits.length > 0
|
|
176
|
+
const activeTier = sourceTierFromContextPack(input.contextPack) ?? (semanticHits.length > 0
|
|
170
177
|
? 'semantic_layer'
|
|
171
178
|
: manifestHits.length > 0
|
|
172
179
|
? 'dbt_manifest'
|
|
173
|
-
: 'dbt_manifest';
|
|
180
|
+
: 'dbt_manifest');
|
|
174
181
|
const reviewRequiredArtifactHits = artifactHits
|
|
175
182
|
.filter((hit) => hit.score >= CERTIFIED_HIT_THRESHOLD && !isCertifiedHit(hit, kg))
|
|
176
183
|
.slice(0, 4);
|
|
@@ -192,13 +199,18 @@ export async function answer(input) {
|
|
|
192
199
|
messages.push({ role: 'system', content: skillsPrompt });
|
|
193
200
|
messages.push({
|
|
194
201
|
role: 'system',
|
|
195
|
-
content: renderContextPrompt(contextBlocks, contextBusiness, contextOther, activeTier, input.memoryContext ?? [], input.extraContext, input.followUp, input.schemaContext ?? [], intent),
|
|
202
|
+
content: renderContextPrompt(contextBlocks, contextBusiness, contextOther, activeTier, input.memoryContext ?? [], input.extraContext, input.followUp, input.schemaContext ?? [], intent, input.contextPack),
|
|
196
203
|
});
|
|
197
204
|
messages.push({ role: 'user', content: question });
|
|
198
205
|
const localProposal = buildSchemaAwareProposal({
|
|
199
206
|
question,
|
|
200
207
|
intent,
|
|
201
208
|
schemaContext: input.schemaContext ?? [],
|
|
209
|
+
}) ?? buildContextPackAwareProposal({
|
|
210
|
+
question,
|
|
211
|
+
intent,
|
|
212
|
+
contextPack: input.contextPack,
|
|
213
|
+
followUp: input.followUp,
|
|
202
214
|
});
|
|
203
215
|
let proposed = '';
|
|
204
216
|
let parsed;
|
|
@@ -231,6 +243,7 @@ export async function answer(input) {
|
|
|
231
243
|
considered,
|
|
232
244
|
memoryContext: input.memoryContext ?? [],
|
|
233
245
|
}),
|
|
246
|
+
contextPack: input.contextPack,
|
|
234
247
|
considered,
|
|
235
248
|
providerUsed: provider.name,
|
|
236
249
|
};
|
|
@@ -259,11 +272,13 @@ export async function answer(input) {
|
|
|
259
272
|
considered,
|
|
260
273
|
memoryContext: input.memoryContext ?? [],
|
|
261
274
|
}),
|
|
275
|
+
contextPack: input.contextPack,
|
|
262
276
|
considered,
|
|
263
277
|
providerUsed: provider.name,
|
|
264
278
|
};
|
|
265
279
|
}
|
|
266
280
|
const generatedCitations = [
|
|
281
|
+
...contextPackCitations(input.contextPack, 4),
|
|
267
282
|
...contextNodes.slice(0, 4).map((n) => ({
|
|
268
283
|
nodeId: n.nodeId,
|
|
269
284
|
kind: n.kind,
|
|
@@ -303,7 +318,7 @@ export async function answer(input) {
|
|
|
303
318
|
executionError = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
304
319
|
}
|
|
305
320
|
}
|
|
306
|
-
if (executionError) {
|
|
321
|
+
if (executionError && hasUsableRepairSchema(input.schemaContext ?? [])) {
|
|
307
322
|
const repairedRaw = await requestSqlRepair({
|
|
308
323
|
provider,
|
|
309
324
|
baseMessages: messages,
|
|
@@ -316,7 +331,6 @@ export async function answer(input) {
|
|
|
316
331
|
const repaired = parseProposal(repairedRaw);
|
|
317
332
|
if (repaired.sql) {
|
|
318
333
|
repairAttempts += 1;
|
|
319
|
-
parsed.text = repaired.text || parsed.text;
|
|
320
334
|
parsed.sql = repaired.sql;
|
|
321
335
|
parsed.viz = repaired.viz ?? parsed.viz;
|
|
322
336
|
try {
|
|
@@ -331,19 +345,22 @@ export async function answer(input) {
|
|
|
331
345
|
}
|
|
332
346
|
}
|
|
333
347
|
}
|
|
348
|
+
if (executionError) {
|
|
349
|
+
parsed.text = composeGeneratedExecutionFailureText(question, executionError);
|
|
350
|
+
}
|
|
334
351
|
const analysisPlan = buildAnalysisPlan({
|
|
335
352
|
question,
|
|
336
353
|
intent,
|
|
337
|
-
routeReason: intent === 'drillthrough'
|
|
354
|
+
routeReason: catalogRoute?.reason ?? (intent === 'drillthrough'
|
|
338
355
|
? 'The user asked for a drill-through or follow-up, so DQL generated review-required SQL from the prior context and current metadata.'
|
|
339
|
-
: 'The question asks for a custom analysis, ranking, breakdown, comparison, or grain that should not be answered by a loose certified block match.',
|
|
356
|
+
: 'The question asks for a custom analysis, ranking, breakdown, comparison, or grain that should not be answered by a loose certified block match.'),
|
|
340
357
|
selectedNodes: contextNodes,
|
|
341
358
|
schemaContext: input.schemaContext ?? [],
|
|
342
359
|
sql: parsed.sql,
|
|
343
360
|
suggestedViz: parsed.viz ?? 'table',
|
|
344
361
|
assumptions: [
|
|
345
362
|
'Generated SQL is an uncertified preview until an analyst reviews and promotes it.',
|
|
346
|
-
...(localProposal ? ['A
|
|
363
|
+
...(localProposal ? ['A local metadata planner selected a review-required SQL grain before provider generation.'] : []),
|
|
347
364
|
...(executionError ? ['The preview execution error must be reviewed before reuse.'] : []),
|
|
348
365
|
],
|
|
349
366
|
repairAttempts,
|
|
@@ -382,6 +399,7 @@ export async function answer(input) {
|
|
|
382
399
|
executorWasAvailable: Boolean(input.executeGeneratedSql),
|
|
383
400
|
analysisPlan,
|
|
384
401
|
}),
|
|
402
|
+
contextPack: input.contextPack,
|
|
385
403
|
considered,
|
|
386
404
|
providerUsed: localProposal ? 'schema_planner' : provider.name,
|
|
387
405
|
};
|
|
@@ -410,7 +428,7 @@ Rules:
|
|
|
410
428
|
PRAGMA, SET, or multiple statements.
|
|
411
429
|
8. If the schema is insufficient to answer, say so explicitly and ask a
|
|
412
430
|
clarifying question instead of guessing.`;
|
|
413
|
-
function renderContextPrompt(blocks, businessContext, others, activeTier, memoryContext, extraContext, followUp, schemaContext = [], intent = 'ad_hoc_analysis') {
|
|
431
|
+
function renderContextPrompt(blocks, businessContext, others, activeTier, memoryContext, extraContext, followUp, schemaContext = [], intent = 'ad_hoc_analysis', contextPack) {
|
|
414
432
|
const intentSection = `## Routing intent\n\nintent: ${intent}\n${intent === 'exact_certified_lookup'
|
|
415
433
|
? 'Use a certified artifact only if it exactly answers the question.'
|
|
416
434
|
: 'Generate review-required SQL for this question. Certified blocks are trusted context, not a reason to answer the wrong grain.'}`;
|
|
@@ -458,7 +476,154 @@ function renderContextPrompt(blocks, businessContext, others, activeTier, memory
|
|
|
458
476
|
const followUpSection = followUp
|
|
459
477
|
? `\n\n## Follow-up routing context\n\n${renderFollowUpContext(followUp)}`
|
|
460
478
|
: '';
|
|
461
|
-
|
|
479
|
+
const contextPackSection = contextPack
|
|
480
|
+
? `\n\n## Local metadata context pack\n\n${renderContextPackForPrompt(contextPack)}`
|
|
481
|
+
: '';
|
|
482
|
+
return `${intentSection}\n\n${blockSection}${businessSection}${otherSection}${schemaSection}${contextPackSection}${memorySection}${extraSection}${followUpSection}`;
|
|
483
|
+
}
|
|
484
|
+
function renderContextPackForPrompt(contextPack) {
|
|
485
|
+
const warnings = contextPack.warnings.length
|
|
486
|
+
? `Warnings:\n${contextPack.warnings.slice(0, 8).map((warning) => `- ${warning}`).join('\n')}\n`
|
|
487
|
+
: '';
|
|
488
|
+
const objects = contextPack.objects.slice(0, 18).map((object) => {
|
|
489
|
+
const detail = [
|
|
490
|
+
object.objectType,
|
|
491
|
+
object.domain ? `domain: ${object.domain}` : '',
|
|
492
|
+
object.status ? `status: ${object.status}` : '',
|
|
493
|
+
object.description ? `description: ${object.description}` : '',
|
|
494
|
+
].filter(Boolean).join('; ');
|
|
495
|
+
return `- ${object.objectKey} (${detail})`;
|
|
496
|
+
}).join('\n');
|
|
497
|
+
const conflicts = contextPack.retrievalDiagnostics.candidateConflicts.length
|
|
498
|
+
? `\nCandidate conflicts:\n${contextPack.retrievalDiagnostics.candidateConflicts.slice(0, 4).map((conflict) => `- ${conflict.reason} ${conflict.prompt}`).join('\n')}`
|
|
499
|
+
: '';
|
|
500
|
+
const route = contextPack.routeDecision
|
|
501
|
+
? `\nRoute decision: ${contextPack.routeDecision.route} / ${contextPack.routeDecision.intent}\nReason: ${contextPack.routeDecision.reason}\nMissing context: ${contextPack.routeDecision.missingContext.map((item) => item.message).join(' ') || 'none'}`
|
|
502
|
+
: '';
|
|
503
|
+
const allowed = contextPack.allowedSqlContext?.relations.length
|
|
504
|
+
? `\nAllowed SQL relations:\n${contextPack.allowedSqlContext.relations.slice(0, 12).map((relation) => `- ${relation.relation}: ${relation.columns.slice(0, 24).map((column) => column.name).join(', ') || '(columns unavailable)'}`).join('\n')}`
|
|
505
|
+
: '';
|
|
506
|
+
const sourceSql = contextPack.allowedSqlContext?.sourceBlockSql.length
|
|
507
|
+
? `\nSource block SQL for review-required drafts:\n${contextPack.allowedSqlContext.sourceBlockSql.slice(0, 4).map((source) => `- ${source.name}${source.status ? ` (${source.status})` : ''}:\n${indentSqlForPrompt(source.sql)}`).join('\n')}`
|
|
508
|
+
: '';
|
|
509
|
+
return [
|
|
510
|
+
`context_pack_id: ${contextPack.id}`,
|
|
511
|
+
`trust_label: ${contextPack.trustLabel}`,
|
|
512
|
+
route.trim(),
|
|
513
|
+
warnings.trim(),
|
|
514
|
+
`Selected evidence:\n${objects || '- none'}`,
|
|
515
|
+
allowed.trim(),
|
|
516
|
+
sourceSql.trim(),
|
|
517
|
+
conflicts.trim(),
|
|
518
|
+
].filter(Boolean).join('\n');
|
|
519
|
+
}
|
|
520
|
+
function indentSqlForPrompt(sql) {
|
|
521
|
+
const normalized = sql.trim().slice(0, 1600);
|
|
522
|
+
return normalized.split(/\r?\n/).map((line) => ` ${line}`).join('\n');
|
|
523
|
+
}
|
|
524
|
+
function contextPackCitations(contextPack, limit) {
|
|
525
|
+
if (!contextPack)
|
|
526
|
+
return [];
|
|
527
|
+
return contextPack.objects.slice(0, limit).map((object) => ({
|
|
528
|
+
nodeId: object.objectKey,
|
|
529
|
+
kind: metadataObjectKindForCitation(object.objectType),
|
|
530
|
+
name: object.name,
|
|
531
|
+
sourceTier: metadataObjectSourceTier(object.objectType),
|
|
532
|
+
provenance: object.sourceSystem,
|
|
533
|
+
}));
|
|
534
|
+
}
|
|
535
|
+
function agentIntentFromCatalogRoute(route) {
|
|
536
|
+
if (route.route === 'clarify')
|
|
537
|
+
return 'clarify';
|
|
538
|
+
if (route.route === 'certified')
|
|
539
|
+
return route.intent === 'definition_lookup' ? 'definition_lookup' : 'exact_certified_lookup';
|
|
540
|
+
return route.intent;
|
|
541
|
+
}
|
|
542
|
+
function shouldUseCertifiedRoute(route, intent) {
|
|
543
|
+
if (route)
|
|
544
|
+
return route.route === 'certified';
|
|
545
|
+
return intent === 'exact_certified_lookup' || intent === 'definition_lookup';
|
|
546
|
+
}
|
|
547
|
+
function certifiedHitFromContextPack(contextPack, kg) {
|
|
548
|
+
const key = contextPack?.routeDecision.exactObjectKey;
|
|
549
|
+
if (!key)
|
|
550
|
+
return null;
|
|
551
|
+
const object = contextPack.objects.find((item) => item.objectKey === key);
|
|
552
|
+
if (!object)
|
|
553
|
+
return null;
|
|
554
|
+
const nodeId = object.objectType === 'dql_block'
|
|
555
|
+
? `block:${object.name}`
|
|
556
|
+
: object.objectType === 'dql_term'
|
|
557
|
+
? `term:${object.name}`
|
|
558
|
+
: object.objectType === 'business_view'
|
|
559
|
+
? `business_view:${object.name}`
|
|
560
|
+
: undefined;
|
|
561
|
+
const node = nodeId ? kg.getNode(nodeId) : null;
|
|
562
|
+
return node ? { node, score: 1, snippet: object.snippet } : null;
|
|
563
|
+
}
|
|
564
|
+
function composeCatalogClarificationText(question, route) {
|
|
565
|
+
if (!route?.missingContext.length)
|
|
566
|
+
return undefined;
|
|
567
|
+
const missing = route.missingContext.map((item) => item.message).join(' ');
|
|
568
|
+
const followUp = route.followUps[0] ? ` ${route.followUps[0]}?` : '';
|
|
569
|
+
return `I need one more detail before querying "${question}". ${missing}${followUp}`;
|
|
570
|
+
}
|
|
571
|
+
function sourceTierFromContextPack(contextPack) {
|
|
572
|
+
if (!contextPack)
|
|
573
|
+
return undefined;
|
|
574
|
+
if (contextPack.objects.some((object) => object.objectType === 'semantic_metric'))
|
|
575
|
+
return 'semantic_layer';
|
|
576
|
+
if (contextPack.objects.some((object) => object.objectType.startsWith('dbt_') || object.objectType === 'warehouse_table' || object.objectType === 'runtime_table'))
|
|
577
|
+
return 'dbt_manifest';
|
|
578
|
+
if (contextPack.objects.some((object) => object.objectType === 'dql_term' || object.objectType === 'business_view'))
|
|
579
|
+
return 'business_context';
|
|
580
|
+
if (contextPack.objects.some((object) => object.objectType === 'dql_block'))
|
|
581
|
+
return 'certified_artifact';
|
|
582
|
+
return undefined;
|
|
583
|
+
}
|
|
584
|
+
function isGeneratedAgentIntent(intent) {
|
|
585
|
+
return intent === 'ad_hoc_analysis'
|
|
586
|
+
|| intent === 'drillthrough'
|
|
587
|
+
|| intent === 'ad_hoc_ranking'
|
|
588
|
+
|| intent === 'driver_breakdown'
|
|
589
|
+
|| intent === 'diagnose_change'
|
|
590
|
+
|| intent === 'segment_compare'
|
|
591
|
+
|| intent === 'entity_drilldown'
|
|
592
|
+
|| intent === 'anomaly_investigation';
|
|
593
|
+
}
|
|
594
|
+
function metadataObjectKindForCitation(objectType) {
|
|
595
|
+
if (objectType === 'dql_block')
|
|
596
|
+
return 'block';
|
|
597
|
+
if (objectType === 'dql_term')
|
|
598
|
+
return 'term';
|
|
599
|
+
if (objectType === 'business_view')
|
|
600
|
+
return 'business_view';
|
|
601
|
+
if (objectType === 'semantic_metric')
|
|
602
|
+
return 'metric';
|
|
603
|
+
if (objectType === 'semantic_dimension')
|
|
604
|
+
return 'dimension';
|
|
605
|
+
if (objectType === 'dbt_model')
|
|
606
|
+
return 'dbt_model';
|
|
607
|
+
if (objectType === 'dbt_source' || objectType === 'warehouse_table')
|
|
608
|
+
return 'dbt_source';
|
|
609
|
+
if (objectType === 'notebook')
|
|
610
|
+
return 'notebook';
|
|
611
|
+
if (objectType === 'dashboard')
|
|
612
|
+
return 'dashboard';
|
|
613
|
+
if (objectType === 'app')
|
|
614
|
+
return 'app';
|
|
615
|
+
return 'runtime_schema';
|
|
616
|
+
}
|
|
617
|
+
function metadataObjectSourceTier(objectType) {
|
|
618
|
+
if (objectType === 'dql_block')
|
|
619
|
+
return 'certified_artifact';
|
|
620
|
+
if (objectType === 'dql_term' || objectType === 'business_view')
|
|
621
|
+
return 'business_context';
|
|
622
|
+
if (objectType.startsWith('semantic_'))
|
|
623
|
+
return 'semantic_layer';
|
|
624
|
+
if (objectType.startsWith('dbt_') || objectType === 'warehouse_table')
|
|
625
|
+
return 'dbt_manifest';
|
|
626
|
+
return 'business_context';
|
|
462
627
|
}
|
|
463
628
|
function renderFollowUpContext(followUp) {
|
|
464
629
|
const parts = [
|
|
@@ -491,7 +656,7 @@ export function parseProposal(raw) {
|
|
|
491
656
|
return { text, sql, viz };
|
|
492
657
|
}
|
|
493
658
|
function buildSchemaAwareProposal(input) {
|
|
494
|
-
if (input.intent
|
|
659
|
+
if (!isGeneratedAgentIntent(input.intent))
|
|
495
660
|
return undefined;
|
|
496
661
|
if (isFilteredEntityQuestion(input.question))
|
|
497
662
|
return undefined;
|
|
@@ -548,6 +713,115 @@ function buildSchemaAwareProposal(input) {
|
|
|
548
713
|
viz: 'table',
|
|
549
714
|
};
|
|
550
715
|
}
|
|
716
|
+
function buildContextPackAwareProposal(input) {
|
|
717
|
+
if (!isGeneratedAgentIntent(input.intent))
|
|
718
|
+
return undefined;
|
|
719
|
+
if (!input.contextPack)
|
|
720
|
+
return undefined;
|
|
721
|
+
const contextPack = input.contextPack;
|
|
722
|
+
const lower = input.question.toLowerCase();
|
|
723
|
+
const filteredSourceProposal = buildSourceBlockFilterProposal({
|
|
724
|
+
question: input.question,
|
|
725
|
+
contextPack,
|
|
726
|
+
followUp: input.followUp,
|
|
727
|
+
});
|
|
728
|
+
if (filteredSourceProposal)
|
|
729
|
+
return filteredSourceProposal;
|
|
730
|
+
if (!/\b(least|lowest|fewest|bottom|min(?:imum)?)\b/.test(lower))
|
|
731
|
+
return undefined;
|
|
732
|
+
for (const object of input.contextPack.objects) {
|
|
733
|
+
if (object.objectType !== 'dql_block' || object.status !== 'certified')
|
|
734
|
+
continue;
|
|
735
|
+
const sql = typeof object.payload?.sql === 'string' ? object.payload.sql.trim() : '';
|
|
736
|
+
if (!sql || !/\border\s+by\b/i.test(sql) || !/\bdesc\b/i.test(sql))
|
|
737
|
+
continue;
|
|
738
|
+
const inverted = invertRankingSql(sql);
|
|
739
|
+
if (!inverted || inverted === sql)
|
|
740
|
+
continue;
|
|
741
|
+
return {
|
|
742
|
+
text: `Generated a review-required least-ranking query by using certified block "${object.name}" as context and reversing its ranking direction. This result is uncertified until reviewed and promoted.`,
|
|
743
|
+
sql: ensurePreviewLimit(inverted, 10),
|
|
744
|
+
viz: 'table',
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
return undefined;
|
|
748
|
+
}
|
|
749
|
+
function buildSourceBlockFilterProposal(input) {
|
|
750
|
+
const year = extractYearFilterValue(input.question, input.followUp);
|
|
751
|
+
if (!year)
|
|
752
|
+
return undefined;
|
|
753
|
+
const source = preferredSourceBlockSql(input.contextPack, input.followUp?.sourceBlockName);
|
|
754
|
+
if (!source?.sql.trim())
|
|
755
|
+
return undefined;
|
|
756
|
+
const sql = source.sql.trim();
|
|
757
|
+
const filterColumn = pickTimeFilterColumn(sql);
|
|
758
|
+
if (!filterColumn)
|
|
759
|
+
return undefined;
|
|
760
|
+
return {
|
|
761
|
+
text: `Generated a review-required ${year} view from certified block "${source.name}" as context. This filters the selected block grain instead of reusing the certified answer directly, so it remains uncertified until reviewed.`,
|
|
762
|
+
sql: [
|
|
763
|
+
'SELECT *',
|
|
764
|
+
'FROM (',
|
|
765
|
+
indentSubquerySql(stripFinalLimit(sql)),
|
|
766
|
+
') AS dql_source',
|
|
767
|
+
`WHERE ${sqlIdentifier(filterColumn)} = ${year}`,
|
|
768
|
+
'LIMIT 200',
|
|
769
|
+
].join('\n'),
|
|
770
|
+
viz: 'table',
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
function preferredSourceBlockSql(contextPack, sourceBlockName) {
|
|
774
|
+
const sources = contextPack.allowedSqlContext?.sourceBlockSql ?? [];
|
|
775
|
+
const normalizedSource = sourceBlockName ? normalizeSourceName(sourceBlockName) : undefined;
|
|
776
|
+
if (normalizedSource) {
|
|
777
|
+
const exact = sources.find((source) => normalizeSourceName(source.name) === normalizedSource);
|
|
778
|
+
if (exact)
|
|
779
|
+
return exact;
|
|
780
|
+
}
|
|
781
|
+
return sources.find((source) => source.status === 'certified') ?? sources[0];
|
|
782
|
+
}
|
|
783
|
+
function extractYearFilterValue(question, followUp) {
|
|
784
|
+
const direct = question.match(/\b(19|20)\d{2}\b/)?.[0];
|
|
785
|
+
if (direct)
|
|
786
|
+
return direct;
|
|
787
|
+
const fromFollowUp = followUp?.filters
|
|
788
|
+
?.map((filter) => filter.match(/\b(19|20)\d{2}\b/)?.[0])
|
|
789
|
+
.find(Boolean);
|
|
790
|
+
return fromFollowUp;
|
|
791
|
+
}
|
|
792
|
+
function pickTimeFilterColumn(sql) {
|
|
793
|
+
if (/\bseason\b/i.test(sql))
|
|
794
|
+
return 'season';
|
|
795
|
+
if (/\byear\b/i.test(sql))
|
|
796
|
+
return 'year';
|
|
797
|
+
return undefined;
|
|
798
|
+
}
|
|
799
|
+
function stripFinalLimit(sql) {
|
|
800
|
+
return sql.replace(/;\s*$/, '').replace(/\s+limit\s+\d+\s*$/i, '').trim();
|
|
801
|
+
}
|
|
802
|
+
function indentSubquerySql(sql) {
|
|
803
|
+
return sql.split(/\r?\n/).map((line) => ` ${line}`).join('\n');
|
|
804
|
+
}
|
|
805
|
+
function normalizeSourceName(value) {
|
|
806
|
+
return value.replace(/^block:/i, '').trim().toLowerCase();
|
|
807
|
+
}
|
|
808
|
+
function invertRankingSql(sql) {
|
|
809
|
+
const withoutTrailingSemicolon = sql.replace(/;\s*$/, '').trim();
|
|
810
|
+
const inverted = withoutTrailingSemicolon.replace(/\border\s+by\s+([\s\S]*?)(\blimit\b|$)/i, (match, orderExpr, limitKeyword) => {
|
|
811
|
+
if (!/\bdesc\b/i.test(orderExpr))
|
|
812
|
+
return match;
|
|
813
|
+
const nextExpr = orderExpr
|
|
814
|
+
.replace(/\bDESC\b/gi, 'ASC')
|
|
815
|
+
.replace(/\bNULLS\s+FIRST\b/gi, 'NULLS LAST');
|
|
816
|
+
return `ORDER BY ${nextExpr}${limitKeyword}`;
|
|
817
|
+
});
|
|
818
|
+
return inverted !== withoutTrailingSemicolon ? inverted : undefined;
|
|
819
|
+
}
|
|
820
|
+
function ensurePreviewLimit(sql, limit) {
|
|
821
|
+
if (/\blimit\s+\d+\b/i.test(sql))
|
|
822
|
+
return sql;
|
|
823
|
+
return `${sql.replace(/;\s*$/, '').trim()}\nLIMIT ${limit}`;
|
|
824
|
+
}
|
|
551
825
|
function findSchemaTable(schemaContext, names) {
|
|
552
826
|
return schemaContext.find((table) => {
|
|
553
827
|
const tableNames = new Set([table.name, table.relation.split('.').at(-1) ?? table.relation].map((name) => name.toLowerCase()));
|
|
@@ -588,7 +862,7 @@ function pickCertifiedArtifact(input) {
|
|
|
588
862
|
// user at a specific block. We still validate it's certified.
|
|
589
863
|
for (const hint of input.blockHints) {
|
|
590
864
|
const node = input.kg.getNode(`block:${hint}`);
|
|
591
|
-
if (node && node.status === 'certified') {
|
|
865
|
+
if (node && node.status === 'certified' && hasCompatibleCertifiedBlockMatch(input.question, node)) {
|
|
592
866
|
return { node, score: 1, snippet: undefined };
|
|
593
867
|
}
|
|
594
868
|
}
|
|
@@ -628,7 +902,7 @@ function pickFirstCertifiedHit(hits, kg, excludedNodeIds, question) {
|
|
|
628
902
|
continue;
|
|
629
903
|
if (!isCertifiedHit(hit, kg))
|
|
630
904
|
continue;
|
|
631
|
-
if (question && hit.node.kind === 'block' && !
|
|
905
|
+
if (question && hit.node.kind === 'block' && !hasCompatibleCertifiedBlockMatch(question, hit.node))
|
|
632
906
|
continue;
|
|
633
907
|
return hit;
|
|
634
908
|
}
|
|
@@ -711,6 +985,44 @@ function hasMeaningfulCertifiedBlockSignal(question, node) {
|
|
|
711
985
|
}
|
|
712
986
|
return false;
|
|
713
987
|
}
|
|
988
|
+
function hasCompatibleCertifiedBlockMatch(question, node) {
|
|
989
|
+
return hasMeaningfulCertifiedBlockSignal(question, node)
|
|
990
|
+
&& hasCompatibleRankingDirection(question, node);
|
|
991
|
+
}
|
|
992
|
+
function hasCompatibleRankingDirection(question, node) {
|
|
993
|
+
const questionDirection = rankingDirectionFromText(question);
|
|
994
|
+
if (!questionDirection)
|
|
995
|
+
return true;
|
|
996
|
+
const blockDirection = rankingDirectionFromText(certifiedBlockSignalText(node));
|
|
997
|
+
if (!blockDirection)
|
|
998
|
+
return true;
|
|
999
|
+
return questionDirection === blockDirection;
|
|
1000
|
+
}
|
|
1001
|
+
function rankingDirectionFromText(text) {
|
|
1002
|
+
const lower = text.toLowerCase();
|
|
1003
|
+
const hasBottomSignal = /\b(bottom|least|fewest|lowest|minimum|min|smallest|worst|underperform(?:ing|ed|er|ers)?)\b/.test(lower);
|
|
1004
|
+
const hasTopSignal = /\b(top|most|highest|maximum|max|greatest|best|leader|leaders|leading)\b/.test(lower);
|
|
1005
|
+
if (hasBottomSignal && !hasTopSignal)
|
|
1006
|
+
return 'bottom';
|
|
1007
|
+
if (hasTopSignal && !hasBottomSignal)
|
|
1008
|
+
return 'top';
|
|
1009
|
+
return undefined;
|
|
1010
|
+
}
|
|
1011
|
+
function certifiedBlockSignalText(node) {
|
|
1012
|
+
const examples = (node.examples ?? [])
|
|
1013
|
+
.flatMap((example) => [example.question, example.sql ?? '']);
|
|
1014
|
+
return [
|
|
1015
|
+
node.name,
|
|
1016
|
+
node.domain ?? '',
|
|
1017
|
+
node.description ?? '',
|
|
1018
|
+
node.llmContext ?? '',
|
|
1019
|
+
node.provenance ?? '',
|
|
1020
|
+
...(node.tags ?? []),
|
|
1021
|
+
...(node.businessRules ?? []),
|
|
1022
|
+
...(node.caveats ?? []),
|
|
1023
|
+
...examples,
|
|
1024
|
+
].join(' ');
|
|
1025
|
+
}
|
|
714
1026
|
function hasExactExecutableArtifactSignal(question, node) {
|
|
715
1027
|
if (!EXECUTABLE_ARTIFACT_KINDS.includes(node.kind))
|
|
716
1028
|
return false;
|
|
@@ -876,7 +1188,7 @@ function isAdHocAnalysisQuestion(question) {
|
|
|
876
1188
|
const lower = question.toLowerCase();
|
|
877
1189
|
if (isBusinessDefinitionQuestion(question))
|
|
878
1190
|
return false;
|
|
879
|
-
return /\b(break\s*down|breakdown|drill\s*(?:down|into)|slice|segment|split|compare|versus|vs\.?|trend|over time|top|bottom|best|worst|highest|lowest|rank|ranking|performed better|better performing|why|what drove|driver|drivers|top movers?|changed?|change|dropped?|drop|decreased?|decrease|declined?|decline|increased?|increase|anomal(?:y|ies)|exceptions?|root cause|contribut(?:e|ed|ion)|variance|delta|by\s+[a-z][\w\s-]{1,40})\b/i.test(lower)
|
|
1191
|
+
return /\b(break\s*down|breakdown|drill\s*(?:down|into)|slice|segment|split|compare|versus|vs\.?|trend|over time|top|bottom|best|worst|highest|lowest|least|fewest|minimum|min|smallest|rank|ranking|performed better|better performing|why|what drove|driver|drivers|top movers?|changed?|change|dropped?|drop|decreased?|decrease|declined?|decline|increased?|increase|anomal(?:y|ies)|exceptions?|root cause|contribut(?:e|ed|ion)|variance|delta|by\s+[a-z][\w\s-]{1,40})\b/i.test(lower)
|
|
880
1192
|
|| /\b(show|list|find|give)\b.+\b(account|accounts|customer|customers|product|products|order|orders|region|location|month|week|day|user|users)\b/i.test(lower);
|
|
881
1193
|
}
|
|
882
1194
|
function isFilteredEntityQuestion(question) {
|
|
@@ -1047,12 +1359,26 @@ async function requestSqlRepair(input) {
|
|
|
1047
1359
|
'The generated SQL failed during bounded preview execution.',
|
|
1048
1360
|
`Question: ${input.question}`,
|
|
1049
1361
|
`Execution error: ${input.executionError}`,
|
|
1050
|
-
'Return one corrected read-only SQL query using only the runtime schema below.',
|
|
1362
|
+
'Return only one corrected read-only SQL query in a fenced sql block using only the runtime schema below.',
|
|
1363
|
+
'If the runtime schema is not enough to repair the SQL, return "NEEDS_CONTEXT:" followed by one short missing-context question. Do not propose proxy tables.',
|
|
1051
1364
|
schema,
|
|
1052
1365
|
].join('\n\n'),
|
|
1053
1366
|
},
|
|
1054
1367
|
], { signal: input.signal });
|
|
1055
1368
|
}
|
|
1369
|
+
function hasUsableRepairSchema(schemaContext) {
|
|
1370
|
+
return schemaContext.some((table) => table.columns.length > 0);
|
|
1371
|
+
}
|
|
1372
|
+
function composeGeneratedExecutionFailureText(question, executionError) {
|
|
1373
|
+
const compactError = executionError.replace(/\s+/g, ' ').trim();
|
|
1374
|
+
const shownError = compactError.length > 220 ? `${compactError.slice(0, 217)}...` : compactError;
|
|
1375
|
+
return [
|
|
1376
|
+
`I generated a review-required SQL draft for "${question}", but the bounded preview did not run successfully.`,
|
|
1377
|
+
'I did not switch to a proxy table because that could answer a different question.',
|
|
1378
|
+
`Execution issue: ${shownError}`,
|
|
1379
|
+
'Refresh the runtime schema or fix the source relation/columns, then rerun the draft.',
|
|
1380
|
+
].join(' ');
|
|
1381
|
+
}
|
|
1056
1382
|
function isRetryableGeneratedSqlError(error) {
|
|
1057
1383
|
return !/\b(read-only|readonly|select or with|unsafe|delete|insert|update|drop|alter|create|attach|copy|pragma)\b/i.test(error);
|
|
1058
1384
|
}
|