@duckcodeailabs/dql-agent 1.6.6 → 1.6.7
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 +17 -0
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +479 -102
- package/dist/answer-loop.js.map +1 -1
- package/dist/app-builder.d.ts.map +1 -1
- package/dist/app-builder.js +80 -38
- package/dist/app-builder.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/kg/build.d.ts.map +1 -1
- package/dist/kg/build.js +1 -0
- package/dist/kg/build.js.map +1 -1
- package/dist/kg/types.d.ts +2 -0
- package/dist/kg/types.d.ts.map +1 -1
- package/dist/metadata/catalog.d.ts +10 -1
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +125 -67
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/drafts.d.ts +25 -0
- package/dist/metadata/drafts.d.ts.map +1 -0
- package/dist/metadata/drafts.js +108 -0
- package/dist/metadata/drafts.js.map +1 -0
- package/dist/metadata/sql-context-validation.d.ts +28 -0
- package/dist/metadata/sql-context-validation.d.ts.map +1 -0
- package/dist/metadata/sql-context-validation.js +340 -0
- package/dist/metadata/sql-context-validation.js.map +1 -0
- package/package.json +4 -4
package/dist/answer-loop.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* full pipeline.
|
|
16
16
|
*/
|
|
17
17
|
import { buildSkillBlockHints, buildSkillsPrompt } from './skills/loader.js';
|
|
18
|
+
import { validateSqlAgainstLocalContext } from './metadata/sql-context-validation.js';
|
|
18
19
|
const CERTIFIED_HIT_THRESHOLD = 0.18;
|
|
19
20
|
const HARD_NEGATIVE_RATIO = 0.5;
|
|
20
21
|
const EXECUTABLE_ARTIFACT_KINDS = ['block', 'dashboard', 'app', 'notebook'];
|
|
@@ -52,7 +53,16 @@ export async function answer(input) {
|
|
|
52
53
|
const intent = catalogRoute ? agentIntentFromCatalogRoute(catalogRoute) : fallbackIntent;
|
|
53
54
|
// Stage 1: certified artifact match. Blocks can be executed; dashboards,
|
|
54
55
|
// Apps, and notebooks are returned as governed citations/navigation targets.
|
|
55
|
-
const
|
|
56
|
+
const drilldownCertifiedHit = input.followUp?.kind === 'drilldown'
|
|
57
|
+
? pickCertifiedDrilldownArtifact({
|
|
58
|
+
executableArtifactHits,
|
|
59
|
+
question,
|
|
60
|
+
followUp: input.followUp,
|
|
61
|
+
excludedArtifactIds,
|
|
62
|
+
kg,
|
|
63
|
+
})
|
|
64
|
+
: null;
|
|
65
|
+
const artifactHit = drilldownCertifiedHit ?? (shouldUseCertifiedRoute(catalogRoute, intent)
|
|
56
66
|
? certifiedHitFromContextPack(input.contextPack, kg)
|
|
57
67
|
?? pickCertifiedArtifact({
|
|
58
68
|
artifactHits,
|
|
@@ -63,7 +73,7 @@ export async function answer(input) {
|
|
|
63
73
|
excludedArtifactIds,
|
|
64
74
|
kg,
|
|
65
75
|
})
|
|
66
|
-
: null;
|
|
76
|
+
: null);
|
|
67
77
|
if (artifactHit) {
|
|
68
78
|
let result;
|
|
69
79
|
let executionError;
|
|
@@ -110,6 +120,10 @@ export async function answer(input) {
|
|
|
110
120
|
result,
|
|
111
121
|
executionError,
|
|
112
122
|
sql: result?.sql,
|
|
123
|
+
trustLabel: input.contextPack?.trustLabel ?? 'certified',
|
|
124
|
+
sourceCertifiedBlock: artifactHit.node.kind === 'block' ? artifactHit.node.name : undefined,
|
|
125
|
+
contextPackId: input.contextPack?.id,
|
|
126
|
+
selectedEvidence: input.contextPack?.evidenceRoles?.slice(0, 12),
|
|
113
127
|
citations,
|
|
114
128
|
memoryContext: input.memoryContext,
|
|
115
129
|
analysisPlan,
|
|
@@ -206,11 +220,12 @@ export async function answer(input) {
|
|
|
206
220
|
question,
|
|
207
221
|
intent,
|
|
208
222
|
schemaContext: input.schemaContext ?? [],
|
|
223
|
+
followUp: input.followUp,
|
|
224
|
+
contextPack: input.contextPack,
|
|
209
225
|
}) ?? buildContextPackAwareProposal({
|
|
210
226
|
question,
|
|
211
227
|
intent,
|
|
212
228
|
contextPack: input.contextPack,
|
|
213
|
-
followUp: input.followUp,
|
|
214
229
|
});
|
|
215
230
|
let proposed = '';
|
|
216
231
|
let parsed;
|
|
@@ -277,6 +292,60 @@ export async function answer(input) {
|
|
|
277
292
|
providerUsed: provider.name,
|
|
278
293
|
};
|
|
279
294
|
}
|
|
295
|
+
const contextValidation = validateSqlAgainstLocalContext(parsed.sql, input.contextPack, {
|
|
296
|
+
question,
|
|
297
|
+
intent,
|
|
298
|
+
filterValues: input.followUp?.filters,
|
|
299
|
+
});
|
|
300
|
+
if (!contextValidation.ok) {
|
|
301
|
+
const text = `I could not safely prepare this generated SQL from the inspected context. ${contextValidation.error}`;
|
|
302
|
+
const analysisPlan = buildAnalysisPlan({
|
|
303
|
+
question,
|
|
304
|
+
intent,
|
|
305
|
+
routeReason: catalogRoute?.reason ?? 'Generated SQL failed metadata context validation before preview execution or draft capture.',
|
|
306
|
+
selectedNodes: contextNodes,
|
|
307
|
+
schemaContext: input.schemaContext ?? [],
|
|
308
|
+
sql: parsed.sql,
|
|
309
|
+
suggestedViz: parsed.viz ?? 'table',
|
|
310
|
+
assumptions: [
|
|
311
|
+
'Generated SQL was rejected before execution because it did not match inspected metadata context.',
|
|
312
|
+
...contextValidation.warnings,
|
|
313
|
+
],
|
|
314
|
+
});
|
|
315
|
+
return {
|
|
316
|
+
kind: 'no_answer',
|
|
317
|
+
sourceTier: 'no_answer',
|
|
318
|
+
certification: 'analyst_review_required',
|
|
319
|
+
reviewStatus: 'none',
|
|
320
|
+
confidence: 0.15,
|
|
321
|
+
text,
|
|
322
|
+
answer: text,
|
|
323
|
+
proposedSql: parsed.sql,
|
|
324
|
+
sql: parsed.sql,
|
|
325
|
+
trustLabel: input.contextPack?.trustLabel,
|
|
326
|
+
sourceCertifiedBlock: followUpSourceBlock?.name ?? input.followUp?.sourceBlockName,
|
|
327
|
+
contextPackId: input.contextPack?.id,
|
|
328
|
+
validationWarnings: contextValidation.warnings,
|
|
329
|
+
selectedEvidence: input.contextPack?.evidenceRoles?.slice(0, 12),
|
|
330
|
+
citations: [],
|
|
331
|
+
memoryContext: input.memoryContext,
|
|
332
|
+
analysisPlan,
|
|
333
|
+
evidence: buildNoAnswerEvidence({
|
|
334
|
+
question,
|
|
335
|
+
reason: contextValidation.error,
|
|
336
|
+
artifactHits,
|
|
337
|
+
businessHits,
|
|
338
|
+
semanticHits,
|
|
339
|
+
manifestHits,
|
|
340
|
+
considered,
|
|
341
|
+
memoryContext: input.memoryContext ?? [],
|
|
342
|
+
analysisPlan,
|
|
343
|
+
}),
|
|
344
|
+
contextPack: input.contextPack,
|
|
345
|
+
considered,
|
|
346
|
+
providerUsed: localProposal ? 'schema_planner' : provider.name,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
280
349
|
const generatedCitations = [
|
|
281
350
|
...contextPackCitations(input.contextPack, 4),
|
|
282
351
|
...contextNodes.slice(0, 4).map((n) => ({
|
|
@@ -318,7 +387,7 @@ export async function answer(input) {
|
|
|
318
387
|
executionError = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
319
388
|
}
|
|
320
389
|
}
|
|
321
|
-
if (executionError
|
|
390
|
+
if (executionError) {
|
|
322
391
|
const repairedRaw = await requestSqlRepair({
|
|
323
392
|
provider,
|
|
324
393
|
baseMessages: messages,
|
|
@@ -331,6 +400,7 @@ export async function answer(input) {
|
|
|
331
400
|
const repaired = parseProposal(repairedRaw);
|
|
332
401
|
if (repaired.sql) {
|
|
333
402
|
repairAttempts += 1;
|
|
403
|
+
parsed.text = repaired.text || parsed.text;
|
|
334
404
|
parsed.sql = repaired.sql;
|
|
335
405
|
parsed.viz = repaired.viz ?? parsed.viz;
|
|
336
406
|
try {
|
|
@@ -345,9 +415,6 @@ export async function answer(input) {
|
|
|
345
415
|
}
|
|
346
416
|
}
|
|
347
417
|
}
|
|
348
|
-
if (executionError) {
|
|
349
|
-
parsed.text = composeGeneratedExecutionFailureText(question, executionError);
|
|
350
|
-
}
|
|
351
418
|
const analysisPlan = buildAnalysisPlan({
|
|
352
419
|
question,
|
|
353
420
|
intent,
|
|
@@ -365,19 +432,60 @@ export async function answer(input) {
|
|
|
365
432
|
],
|
|
366
433
|
repairAttempts,
|
|
367
434
|
});
|
|
435
|
+
const validationWarnings = [
|
|
436
|
+
...(input.contextPack?.warnings ?? []),
|
|
437
|
+
...(executionError ? ['The preview execution error must be reviewed before reuse.'] : []),
|
|
438
|
+
];
|
|
439
|
+
let draftBlock;
|
|
440
|
+
let draftCaptureError;
|
|
441
|
+
if (input.captureGeneratedDraft && parsed.sql) {
|
|
442
|
+
try {
|
|
443
|
+
draftBlock = await input.captureGeneratedDraft({
|
|
444
|
+
question,
|
|
445
|
+
sql: parsed.sql,
|
|
446
|
+
intent,
|
|
447
|
+
followUp: input.followUp,
|
|
448
|
+
contextPack: input.contextPack,
|
|
449
|
+
sourceBlock: followUpSourceBlock ?? undefined,
|
|
450
|
+
validationWarnings,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
catch (err) {
|
|
454
|
+
draftCaptureError = err instanceof Error ? err.message : String(err);
|
|
455
|
+
validationWarnings.push(`Draft capture failed: ${draftCaptureError}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const sourceCertifiedBlock = followUpSourceBlock?.name ?? input.followUp?.sourceBlockName;
|
|
459
|
+
const trustExplanation = generatedTrustExplanation({
|
|
460
|
+
followUp: input.followUp,
|
|
461
|
+
sourceCertifiedBlock,
|
|
462
|
+
draftBlock,
|
|
463
|
+
});
|
|
464
|
+
const cleanedSummary = cleanGeneratedSummary(parsed.text);
|
|
465
|
+
const generatedText = trustExplanation
|
|
466
|
+
? [trustExplanation, cleanedSummary].filter(Boolean).join('\n\n')
|
|
467
|
+
: cleanedSummary;
|
|
368
468
|
return {
|
|
369
469
|
kind: 'uncertified',
|
|
370
470
|
sourceTier: activeTier,
|
|
371
471
|
certification: 'ai_generated',
|
|
372
472
|
reviewStatus: 'draft_ready',
|
|
373
473
|
confidence: activeTier === 'semantic_layer' ? 0.72 : 0.55,
|
|
374
|
-
text:
|
|
375
|
-
answer:
|
|
474
|
+
text: generatedText,
|
|
475
|
+
answer: generatedText,
|
|
376
476
|
proposedSql: parsed.sql,
|
|
377
477
|
sql: parsed.sql,
|
|
378
478
|
result,
|
|
379
479
|
executionError,
|
|
380
480
|
suggestedViz: parsed.viz ?? 'table',
|
|
481
|
+
draftBlock,
|
|
482
|
+
draftBlockId: draftBlock?.path,
|
|
483
|
+
promoteCommand: draftBlock ? `dql certify --from-draft ${draftBlock.path}` : undefined,
|
|
484
|
+
trustLabel: input.contextPack?.trustLabel,
|
|
485
|
+
sourceCertifiedBlock,
|
|
486
|
+
contextPackId: input.contextPack?.id,
|
|
487
|
+
validationWarnings,
|
|
488
|
+
selectedEvidence: input.contextPack?.evidenceRoles?.slice(0, 12),
|
|
381
489
|
citations: generatedCitations,
|
|
382
490
|
memoryContext: input.memoryContext,
|
|
383
491
|
analysisPlan,
|
|
@@ -427,7 +535,10 @@ Rules:
|
|
|
427
535
|
proposed SQL. Do not emit INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, COPY,
|
|
428
536
|
PRAGMA, SET, or multiple statements.
|
|
429
537
|
8. If the schema is insufficient to answer, say so explicitly and ask a
|
|
430
|
-
clarifying question instead of guessing
|
|
538
|
+
clarifying question instead of guessing.
|
|
539
|
+
9. Write directly to the analyst. Do not say "the user is asking", "the user
|
|
540
|
+
requested", "I will generate", or describe internal routing. State the
|
|
541
|
+
answer, the certified context used, and the review requirement.`;
|
|
431
542
|
function renderContextPrompt(blocks, businessContext, others, activeTier, memoryContext, extraContext, followUp, schemaContext = [], intent = 'ad_hoc_analysis', contextPack) {
|
|
432
543
|
const intentSection = `## Routing intent\n\nintent: ${intent}\n${intent === 'exact_certified_lookup'
|
|
433
544
|
? 'Use a certified artifact only if it exactly answers the question.'
|
|
@@ -503,9 +614,6 @@ function renderContextPackForPrompt(contextPack) {
|
|
|
503
614
|
const allowed = contextPack.allowedSqlContext?.relations.length
|
|
504
615
|
? `\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
616
|
: '';
|
|
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
617
|
return [
|
|
510
618
|
`context_pack_id: ${contextPack.id}`,
|
|
511
619
|
`trust_label: ${contextPack.trustLabel}`,
|
|
@@ -513,14 +621,9 @@ function renderContextPackForPrompt(contextPack) {
|
|
|
513
621
|
warnings.trim(),
|
|
514
622
|
`Selected evidence:\n${objects || '- none'}`,
|
|
515
623
|
allowed.trim(),
|
|
516
|
-
sourceSql.trim(),
|
|
517
624
|
conflicts.trim(),
|
|
518
625
|
].filter(Boolean).join('\n');
|
|
519
626
|
}
|
|
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
627
|
function contextPackCitations(contextPack, limit) {
|
|
525
628
|
if (!contextPack)
|
|
526
629
|
return [];
|
|
@@ -639,6 +742,30 @@ function renderFollowUpContext(followUp) {
|
|
|
639
742
|
: 'routing rule: reuse the prior certified block when the user asks a generic follow-up.';
|
|
640
743
|
return [...parts, rule].join('\n');
|
|
641
744
|
}
|
|
745
|
+
function generatedTrustExplanation(input) {
|
|
746
|
+
if (input.followUp?.kind !== 'drilldown')
|
|
747
|
+
return undefined;
|
|
748
|
+
const source = input.sourceCertifiedBlock
|
|
749
|
+
? ` I used the certified \`${input.sourceCertifiedBlock}\` block for the business definition,`
|
|
750
|
+
: ' I used certified context where available,';
|
|
751
|
+
const filters = [
|
|
752
|
+
...(input.followUp.filters ?? []),
|
|
753
|
+
...(input.followUp.dimensions ?? []),
|
|
754
|
+
];
|
|
755
|
+
const grain = filters.length ? ` at the requested ${filters.join('/')} grain` : ' at the requested drilldown grain';
|
|
756
|
+
const draft = input.draftBlock
|
|
757
|
+
? ` The draft was saved at \`${input.draftBlock.path}\` for review.`
|
|
758
|
+
: ' The generated SQL still needs analyst review before certification.';
|
|
759
|
+
return `This is an uncertified drilldown.${source} then generated new SQL${grain}.${draft}`;
|
|
760
|
+
}
|
|
761
|
+
function cleanGeneratedSummary(text) {
|
|
762
|
+
return text
|
|
763
|
+
.trim()
|
|
764
|
+
.replace(/^(?:the user (?:is asking|asked|wants|requested)[^.]*\.\s*)+/i, '')
|
|
765
|
+
.replace(/\s*(?:therefore,\s*)?i will generate review-required sql[^.]*\.\s*/gi, ' ')
|
|
766
|
+
.replace(/\s*(?:therefore,\s*)?i will generate[^.]*\.\s*/gi, ' ')
|
|
767
|
+
.trim();
|
|
768
|
+
}
|
|
642
769
|
/**
|
|
643
770
|
* Public for tests. Pulls the first ```sql block and an optional Viz: line
|
|
644
771
|
* out of an LLM response.
|
|
@@ -658,6 +785,10 @@ export function parseProposal(raw) {
|
|
|
658
785
|
function buildSchemaAwareProposal(input) {
|
|
659
786
|
if (!isGeneratedAgentIntent(input.intent))
|
|
660
787
|
return undefined;
|
|
788
|
+
const schemaContext = schemaContextWithAllowedSqlContext(input.schemaContext, input.contextPack);
|
|
789
|
+
const drilldownProposal = buildMatchedEntityDrilldownProposal({ ...input, schemaContext });
|
|
790
|
+
if (drilldownProposal)
|
|
791
|
+
return drilldownProposal;
|
|
661
792
|
if (isFilteredEntityQuestion(input.question))
|
|
662
793
|
return undefined;
|
|
663
794
|
const lower = input.question.toLowerCase();
|
|
@@ -666,7 +797,7 @@ function buildSchemaAwareProposal(input) {
|
|
|
666
797
|
&& !/\b(order details|specific orders|each order|all orders|order line|line item)\b/.test(lower);
|
|
667
798
|
if (!asksForCustomerPerformance)
|
|
668
799
|
return undefined;
|
|
669
|
-
const customers = findSchemaTable(
|
|
800
|
+
const customers = findSchemaTable(schemaContext, ['customers', 'customer']);
|
|
670
801
|
if (!customers)
|
|
671
802
|
return undefined;
|
|
672
803
|
const customerName = findSchemaColumn(customers, ['customer_name', 'name', 'full_name']);
|
|
@@ -688,7 +819,7 @@ function buildSchemaAwareProposal(input) {
|
|
|
688
819
|
};
|
|
689
820
|
}
|
|
690
821
|
const customerId = findSchemaColumn(customers, ['customer_id', 'id']);
|
|
691
|
-
const orders = findSchemaTable(
|
|
822
|
+
const orders = findSchemaTable(schemaContext, ['orders', 'order']);
|
|
692
823
|
if (!orders || !customerName || !customerId)
|
|
693
824
|
return undefined;
|
|
694
825
|
const orderCustomerId = findSchemaColumn(orders, ['customer_id', 'customer']);
|
|
@@ -713,20 +844,305 @@ function buildSchemaAwareProposal(input) {
|
|
|
713
844
|
viz: 'table',
|
|
714
845
|
};
|
|
715
846
|
}
|
|
847
|
+
function buildMatchedEntityDrilldownProposal(input) {
|
|
848
|
+
if (input.intent !== 'entity_drilldown' && input.followUp?.kind !== 'drilldown')
|
|
849
|
+
return undefined;
|
|
850
|
+
const table = pickDrilldownTable(input.schemaContext, input.question, input.followUp);
|
|
851
|
+
if (!table)
|
|
852
|
+
return undefined;
|
|
853
|
+
const dimension = inferDrilldownDimension(table, input.question, input.followUp);
|
|
854
|
+
if (!dimension)
|
|
855
|
+
return undefined;
|
|
856
|
+
const entityFilters = matchedEntityFiltersForQuestion(table, input.question, input.followUp);
|
|
857
|
+
if (entityFilters.length === 0)
|
|
858
|
+
return undefined;
|
|
859
|
+
if (entityFilters.some((filter) => namesEqualLoose(filter.column, dimension)))
|
|
860
|
+
return undefined;
|
|
861
|
+
const sourceSql = selectSourceBlockSql(input.contextPack, input.followUp?.sourceBlockName);
|
|
862
|
+
const metric = inferDrilldownMetric(table, input.question, sourceSql);
|
|
863
|
+
if (!metric)
|
|
864
|
+
return undefined;
|
|
865
|
+
const timePredicates = drilldownTimePredicates({
|
|
866
|
+
question: input.question,
|
|
867
|
+
followUp: input.followUp,
|
|
868
|
+
table,
|
|
869
|
+
sourceSql,
|
|
870
|
+
});
|
|
871
|
+
if (mentionsRelativeTime(input.question, input.followUp) && timePredicates.length === 0)
|
|
872
|
+
return undefined;
|
|
873
|
+
const predicates = [
|
|
874
|
+
...entityFilters.map((filter) => `${sqlIdentifier(filter.column)} = ${sqlStringLiteral(filter.value)}`),
|
|
875
|
+
...timePredicates,
|
|
876
|
+
];
|
|
877
|
+
const where = predicates.length ? [`WHERE ${predicates.join(' AND ')}`] : [];
|
|
878
|
+
return {
|
|
879
|
+
text: [
|
|
880
|
+
`Prepared a review-required ${humanizeIdentifier(dimension)} drilldown from inspected metadata.`,
|
|
881
|
+
`The entity filter uses ${entityFilters.map((filter) => `${filter.column} = ${filter.value}`).join(', ')} from matched sample values.`,
|
|
882
|
+
'This result is uncertified until reviewed and promoted.',
|
|
883
|
+
].join(' '),
|
|
884
|
+
sql: [
|
|
885
|
+
'SELECT',
|
|
886
|
+
` ${sqlIdentifier(dimension)} AS ${sqlIdentifier(dimension)},`,
|
|
887
|
+
` ${metric.expression} AS ${sqlIdentifier(metric.alias)}`,
|
|
888
|
+
`FROM ${sqlRelation(table.relation)}`,
|
|
889
|
+
...where,
|
|
890
|
+
`GROUP BY ${sqlIdentifier(dimension)}`,
|
|
891
|
+
`ORDER BY ${sqlIdentifier(metric.alias)} DESC`,
|
|
892
|
+
'LIMIT 50',
|
|
893
|
+
].join('\n'),
|
|
894
|
+
viz: 'bar',
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
function schemaContextWithAllowedSqlContext(schemaContext, contextPack) {
|
|
898
|
+
const byRelation = new Map();
|
|
899
|
+
for (const table of schemaContext) {
|
|
900
|
+
byRelation.set(normalizeRelationKey(table.relation), {
|
|
901
|
+
...table,
|
|
902
|
+
columns: table.columns.map((column) => ({ ...column, sampleValues: column.sampleValues?.slice() })),
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
for (const relation of contextPack?.allowedSqlContext?.relations ?? []) {
|
|
906
|
+
const key = normalizeRelationKey(relation.relation);
|
|
907
|
+
const existing = byRelation.get(key);
|
|
908
|
+
if (!existing) {
|
|
909
|
+
byRelation.set(key, {
|
|
910
|
+
relation: relation.relation,
|
|
911
|
+
name: relation.name,
|
|
912
|
+
columns: relation.columns.map((column) => ({
|
|
913
|
+
name: column.name,
|
|
914
|
+
type: column.type,
|
|
915
|
+
description: column.description,
|
|
916
|
+
sampleValues: column.sampleValues?.slice(),
|
|
917
|
+
})),
|
|
918
|
+
source: relation.source,
|
|
919
|
+
});
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
const columns = new Map(existing.columns.map((column) => [column.name.toLowerCase(), column]));
|
|
923
|
+
for (const column of relation.columns) {
|
|
924
|
+
const existingColumn = columns.get(column.name.toLowerCase());
|
|
925
|
+
if (!existingColumn) {
|
|
926
|
+
existing.columns.push({
|
|
927
|
+
name: column.name,
|
|
928
|
+
type: column.type,
|
|
929
|
+
description: column.description,
|
|
930
|
+
sampleValues: column.sampleValues?.slice(),
|
|
931
|
+
});
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
existingColumn.sampleValues = uniqueDrilldownStrings([
|
|
935
|
+
...(existingColumn.sampleValues ?? []),
|
|
936
|
+
...(column.sampleValues ?? []),
|
|
937
|
+
]).slice(0, 8);
|
|
938
|
+
existingColumn.type ??= column.type;
|
|
939
|
+
existingColumn.description ??= column.description;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
return Array.from(byRelation.values());
|
|
943
|
+
}
|
|
944
|
+
function pickDrilldownTable(schemaContext, question, followUp) {
|
|
945
|
+
const scored = schemaContext
|
|
946
|
+
.map((table) => ({
|
|
947
|
+
table,
|
|
948
|
+
filters: matchedEntityFiltersForQuestion(table, question, followUp).length,
|
|
949
|
+
dimension: inferDrilldownDimension(table, question, followUp) ? 1 : 0,
|
|
950
|
+
measure: inferDrilldownMetric(table, question, undefined) ? 1 : 0,
|
|
951
|
+
}))
|
|
952
|
+
.filter((candidate) => candidate.filters > 0 && candidate.dimension > 0 && candidate.measure > 0)
|
|
953
|
+
.sort((a, b) => (b.filters + b.dimension + b.measure) - (a.filters + a.dimension + a.measure));
|
|
954
|
+
return scored[0]?.table;
|
|
955
|
+
}
|
|
956
|
+
function inferDrilldownDimension(table, question, followUp) {
|
|
957
|
+
const lower = question.toLowerCase();
|
|
958
|
+
const requested = [
|
|
959
|
+
...(followUp?.dimensions ?? []),
|
|
960
|
+
...Array.from(lower.matchAll(/\bby\s+([a-z][a-z0-9_ -]{1,40})/g)).map((match) => match[1] ?? ''),
|
|
961
|
+
]
|
|
962
|
+
.flatMap((value) => value.split(/\band\b|,|\//i))
|
|
963
|
+
.map((value) => value.replace(/\b(last|this|next|previous|prior|current)\s+(day|week|month|quarter|year)\b/gi, '').trim())
|
|
964
|
+
.filter(Boolean);
|
|
965
|
+
const direct = findSchemaColumn(table, requested);
|
|
966
|
+
if (direct)
|
|
967
|
+
return direct;
|
|
968
|
+
if (/\bcustomers?\b/.test(lower)) {
|
|
969
|
+
const customer = findSchemaColumn(table, ['customer', 'customer_name', 'account', 'account_name']);
|
|
970
|
+
if (customer)
|
|
971
|
+
return customer;
|
|
972
|
+
}
|
|
973
|
+
if (/\bsegments?\b/.test(lower)) {
|
|
974
|
+
const segment = findSchemaColumn(table, ['segment', 'customer_segment', 'market_segment']);
|
|
975
|
+
if (segment)
|
|
976
|
+
return segment;
|
|
977
|
+
}
|
|
978
|
+
if (/\bproducts?\b/.test(lower)) {
|
|
979
|
+
const product = findSchemaColumn(table, ['product', 'product_name', 'sku']);
|
|
980
|
+
if (product)
|
|
981
|
+
return product;
|
|
982
|
+
}
|
|
983
|
+
if (/\bregions?\b/.test(lower)) {
|
|
984
|
+
const region = findSchemaColumn(table, ['region', 'market', 'geo']);
|
|
985
|
+
if (region)
|
|
986
|
+
return region;
|
|
987
|
+
}
|
|
988
|
+
return undefined;
|
|
989
|
+
}
|
|
990
|
+
function matchedEntityFiltersForQuestion(table, question, followUp) {
|
|
991
|
+
const text = normalizeForEntityMatch([
|
|
992
|
+
question,
|
|
993
|
+
...(followUp?.filters ?? []),
|
|
994
|
+
].join(' '));
|
|
995
|
+
const filters = [];
|
|
996
|
+
for (const column of table.columns) {
|
|
997
|
+
for (const sampleValue of column.sampleValues ?? []) {
|
|
998
|
+
if (isTemporalDrilldownValue(sampleValue))
|
|
999
|
+
continue;
|
|
1000
|
+
const needle = normalizeForEntityMatch(sampleValue);
|
|
1001
|
+
if (!needle || !text.includes(needle))
|
|
1002
|
+
continue;
|
|
1003
|
+
filters.push({ column: column.name, value: sampleValue });
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return uniqueMatchedEntityFilters(filters);
|
|
1007
|
+
}
|
|
1008
|
+
function inferDrilldownMetric(table, question, sourceSql) {
|
|
1009
|
+
const sourceMetric = sourceSql ? aggregateMetricFromSourceSql(table, sourceSql) : undefined;
|
|
1010
|
+
if (sourceMetric)
|
|
1011
|
+
return sourceMetric;
|
|
1012
|
+
const lower = question.toLowerCase();
|
|
1013
|
+
const candidates = /\brevenue|arr|mrr|sales\b/.test(lower)
|
|
1014
|
+
? ['revenue', 'net_revenue', 'gross_revenue', 'amount', 'order_total', 'total_amount', 'sales', 'arr', 'mrr']
|
|
1015
|
+
: ['amount', 'revenue', 'order_total', 'total_amount', 'value', 'spend'];
|
|
1016
|
+
const column = findSchemaColumn(table, candidates);
|
|
1017
|
+
if (!column)
|
|
1018
|
+
return undefined;
|
|
1019
|
+
const alias = /\brevenue|arr|mrr|sales\b/.test(lower) ? 'revenue_total' : `${column}_total`;
|
|
1020
|
+
return {
|
|
1021
|
+
expression: `SUM(${sqlIdentifier(column)})`,
|
|
1022
|
+
alias,
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
function aggregateMetricFromSourceSql(table, sourceSql) {
|
|
1026
|
+
for (const column of table.columns) {
|
|
1027
|
+
const columnPattern = sqlIdentifierPattern(column.name);
|
|
1028
|
+
const aggregatePattern = new RegExp(`\\b(SUM|COUNT|AVG|MIN|MAX)\\s*\\(\\s*(?:["\`]?\\w+["\`]?\\s*\\.\\s*)?(${columnPattern}|\\*)\\s*\\)\\s*(?:AS\\s+(["\`]?\\w+["\`]?))?`, 'i');
|
|
1029
|
+
const match = sourceSql.match(aggregatePattern);
|
|
1030
|
+
if (!match)
|
|
1031
|
+
continue;
|
|
1032
|
+
const fn = (match[1] ?? 'SUM').toUpperCase();
|
|
1033
|
+
const target = match[2] === '*' ? '*' : sqlIdentifier(column.name);
|
|
1034
|
+
const alias = cleanSqlIdentifier(match[3] ?? defaultMetricAlias(fn, column.name));
|
|
1035
|
+
return {
|
|
1036
|
+
expression: `${fn}(${target})`,
|
|
1037
|
+
alias,
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
return undefined;
|
|
1041
|
+
}
|
|
1042
|
+
function drilldownTimePredicates(input) {
|
|
1043
|
+
if (!mentionsRelativeTime(input.question, input.followUp))
|
|
1044
|
+
return [];
|
|
1045
|
+
if (!input.sourceSql)
|
|
1046
|
+
return [];
|
|
1047
|
+
const timeColumns = input.table.columns.map((column) => column.name).filter(isTimeLikeDrilldownColumn);
|
|
1048
|
+
if (timeColumns.length === 0)
|
|
1049
|
+
return [];
|
|
1050
|
+
return extractWherePredicates(input.sourceSql)
|
|
1051
|
+
.map(stripSqlAliasQualifiers)
|
|
1052
|
+
.filter((predicate) => isReusableSqlPredicate(predicate))
|
|
1053
|
+
.filter((predicate) => timeColumns.some((column) => predicateReferencesColumn(predicate, column)))
|
|
1054
|
+
.slice(0, 3);
|
|
1055
|
+
}
|
|
1056
|
+
function selectSourceBlockSql(contextPack, sourceBlockName) {
|
|
1057
|
+
const sourceSql = contextPack?.allowedSqlContext?.sourceBlockSql ?? [];
|
|
1058
|
+
if (sourceSql.length === 0)
|
|
1059
|
+
return undefined;
|
|
1060
|
+
const preferred = sourceBlockName
|
|
1061
|
+
? sourceSql.find((source) => namesEqualLoose(source.name, sourceBlockName))
|
|
1062
|
+
: undefined;
|
|
1063
|
+
return (preferred ?? sourceSql.find((source) => source.status === 'certified') ?? sourceSql[0])?.sql;
|
|
1064
|
+
}
|
|
1065
|
+
function extractWherePredicates(sql) {
|
|
1066
|
+
const match = sql.match(/\bWHERE\b([\s\S]*?)(\bGROUP\s+BY\b|\bHAVING\b|\bORDER\s+BY\b|\bLIMIT\b|$)/i);
|
|
1067
|
+
if (!match)
|
|
1068
|
+
return [];
|
|
1069
|
+
return (match[1] ?? '')
|
|
1070
|
+
.split(/\s+AND\s+/i)
|
|
1071
|
+
.map((part) => part.trim().replace(/^\(+|\)+$/g, '').replace(/\s+/g, ' '))
|
|
1072
|
+
.filter(Boolean);
|
|
1073
|
+
}
|
|
1074
|
+
function mentionsRelativeTime(question, followUp) {
|
|
1075
|
+
const text = [question, ...(followUp?.filters ?? [])].join(' ');
|
|
1076
|
+
return /\b(last|this|next|previous|prior|current)\s+(day|week|month|quarter|year)\b/i.test(text)
|
|
1077
|
+
|| /\b(today|yesterday|tomorrow|ytd|mtd|qtd|wtd)\b/i.test(text);
|
|
1078
|
+
}
|
|
1079
|
+
function isTimeLikeDrilldownColumn(name) {
|
|
1080
|
+
return /\b(date|time|day|week|month|quarter|year|period|created_at|updated_at)\b/i.test(name);
|
|
1081
|
+
}
|
|
1082
|
+
function stripSqlAliasQualifiers(predicate) {
|
|
1083
|
+
return predicate.replace(/\b["`]?\w+["`]?\s*\.\s*(["`]?\w+["`]?)/g, '$1');
|
|
1084
|
+
}
|
|
1085
|
+
function isReusableSqlPredicate(predicate) {
|
|
1086
|
+
if (!predicate || predicate.length > 240)
|
|
1087
|
+
return false;
|
|
1088
|
+
if (/[;]/.test(predicate) || /--|\/\*/.test(predicate))
|
|
1089
|
+
return false;
|
|
1090
|
+
return !/\b(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|COPY|PRAGMA|SET)\b/i.test(predicate);
|
|
1091
|
+
}
|
|
1092
|
+
function predicateReferencesColumn(predicate, column) {
|
|
1093
|
+
return new RegExp(`(^|[^\\w])${sqlIdentifierPattern(column)}([^\\w]|$)`, 'i').test(predicate);
|
|
1094
|
+
}
|
|
1095
|
+
function sqlIdentifierPattern(identifier) {
|
|
1096
|
+
const escaped = escapeRegExp(identifier);
|
|
1097
|
+
return `(?:"${escaped}"|\`${escaped}\`|${escaped})`;
|
|
1098
|
+
}
|
|
1099
|
+
function sqlStringLiteral(value) {
|
|
1100
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
1101
|
+
}
|
|
1102
|
+
function defaultMetricAlias(fn, column) {
|
|
1103
|
+
if (fn === 'SUM' && /revenue|amount|sales|arr|mrr/i.test(column))
|
|
1104
|
+
return 'revenue_total';
|
|
1105
|
+
return `${column}_${fn.toLowerCase()}`;
|
|
1106
|
+
}
|
|
1107
|
+
function cleanSqlIdentifier(identifier) {
|
|
1108
|
+
return identifier.replace(/^["`]|["`]$/g, '').trim();
|
|
1109
|
+
}
|
|
1110
|
+
function namesEqualLoose(a, b) {
|
|
1111
|
+
return cleanSqlIdentifier(a).replace(/[_-]+/g, ' ').toLowerCase() === cleanSqlIdentifier(b).replace(/[_-]+/g, ' ').toLowerCase();
|
|
1112
|
+
}
|
|
1113
|
+
function normalizeRelationKey(relation) {
|
|
1114
|
+
return relation.replace(/["`]/g, '').replace(/\s*\.\s*/g, '.').toLowerCase().trim();
|
|
1115
|
+
}
|
|
1116
|
+
function normalizeForEntityMatch(value) {
|
|
1117
|
+
return value.toLowerCase().replace(/[^a-z0-9.%+-]+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
1118
|
+
}
|
|
1119
|
+
function uniqueMatchedEntityFilters(filters) {
|
|
1120
|
+
const seen = new Set();
|
|
1121
|
+
const unique = [];
|
|
1122
|
+
for (const filter of filters) {
|
|
1123
|
+
const key = `${filter.column.toLowerCase()}\0${filter.value.toLowerCase()}`;
|
|
1124
|
+
if (seen.has(key))
|
|
1125
|
+
continue;
|
|
1126
|
+
seen.add(key);
|
|
1127
|
+
unique.push(filter);
|
|
1128
|
+
}
|
|
1129
|
+
return unique;
|
|
1130
|
+
}
|
|
1131
|
+
function uniqueDrilldownStrings(values) {
|
|
1132
|
+
return Array.from(new Set(values));
|
|
1133
|
+
}
|
|
1134
|
+
function isTemporalDrilldownValue(value) {
|
|
1135
|
+
return mentionsRelativeTime(value, undefined) || /^\d{4}-\d{2}-\d{2}/.test(value);
|
|
1136
|
+
}
|
|
1137
|
+
function escapeRegExp(value) {
|
|
1138
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1139
|
+
}
|
|
716
1140
|
function buildContextPackAwareProposal(input) {
|
|
717
1141
|
if (!isGeneratedAgentIntent(input.intent))
|
|
718
1142
|
return undefined;
|
|
719
1143
|
if (!input.contextPack)
|
|
720
1144
|
return undefined;
|
|
721
|
-
const contextPack = input.contextPack;
|
|
722
1145
|
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
1146
|
if (!/\b(least|lowest|fewest|bottom|min(?:imum)?)\b/.test(lower))
|
|
731
1147
|
return undefined;
|
|
732
1148
|
for (const object of input.contextPack.objects) {
|
|
@@ -746,65 +1162,6 @@ function buildContextPackAwareProposal(input) {
|
|
|
746
1162
|
}
|
|
747
1163
|
return undefined;
|
|
748
1164
|
}
|
|
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
1165
|
function invertRankingSql(sql) {
|
|
809
1166
|
const withoutTrailingSemicolon = sql.replace(/;\s*$/, '').trim();
|
|
810
1167
|
const inverted = withoutTrailingSemicolon.replace(/\border\s+by\s+([\s\S]*?)(\blimit\b|$)/i, (match, orderExpr, limitKeyword) => {
|
|
@@ -908,6 +1265,40 @@ function pickFirstCertifiedHit(hits, kg, excludedNodeIds, question) {
|
|
|
908
1265
|
}
|
|
909
1266
|
return null;
|
|
910
1267
|
}
|
|
1268
|
+
function pickCertifiedDrilldownArtifact(input) {
|
|
1269
|
+
const requestedTerms = meaningfulTokens([
|
|
1270
|
+
input.question,
|
|
1271
|
+
...(input.followUp.filters ?? []),
|
|
1272
|
+
...(input.followUp.dimensions ?? []),
|
|
1273
|
+
].join(' '));
|
|
1274
|
+
for (const hit of input.executableArtifactHits) {
|
|
1275
|
+
if (hit.score < CERTIFIED_HIT_THRESHOLD)
|
|
1276
|
+
break;
|
|
1277
|
+
if (input.excludedArtifactIds?.has(hit.node.nodeId))
|
|
1278
|
+
continue;
|
|
1279
|
+
if (hit.node.kind !== 'block')
|
|
1280
|
+
continue;
|
|
1281
|
+
if (!isCertifiedHit(hit, input.kg))
|
|
1282
|
+
continue;
|
|
1283
|
+
if (!hasCompatibleCertifiedBlockMatch(input.question, hit.node))
|
|
1284
|
+
continue;
|
|
1285
|
+
if (!hasRequestedDrilldownOverlap(hit.node, requestedTerms))
|
|
1286
|
+
continue;
|
|
1287
|
+
return hit;
|
|
1288
|
+
}
|
|
1289
|
+
return null;
|
|
1290
|
+
}
|
|
1291
|
+
function hasRequestedDrilldownOverlap(node, requestedTerms) {
|
|
1292
|
+
if (requestedTerms.size === 0)
|
|
1293
|
+
return false;
|
|
1294
|
+
const nodeTerms = meaningfulTokens(certifiedBlockSignalText(node));
|
|
1295
|
+
let overlaps = 0;
|
|
1296
|
+
for (const term of requestedTerms) {
|
|
1297
|
+
if (nodeTerms.has(term))
|
|
1298
|
+
overlaps += 1;
|
|
1299
|
+
}
|
|
1300
|
+
return overlaps >= 2 || (requestedTerms.size === 1 && overlaps === 1);
|
|
1301
|
+
}
|
|
911
1302
|
function shouldDeferCertifiedArtifactForReviewPath(input) {
|
|
912
1303
|
if (!isBreakdownOrDrilldownQuestion(input.question))
|
|
913
1304
|
return false;
|
|
@@ -1359,26 +1750,12 @@ async function requestSqlRepair(input) {
|
|
|
1359
1750
|
'The generated SQL failed during bounded preview execution.',
|
|
1360
1751
|
`Question: ${input.question}`,
|
|
1361
1752
|
`Execution error: ${input.executionError}`,
|
|
1362
|
-
'Return
|
|
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.',
|
|
1753
|
+
'Return one corrected read-only SQL query using only the runtime schema below.',
|
|
1364
1754
|
schema,
|
|
1365
1755
|
].join('\n\n'),
|
|
1366
1756
|
},
|
|
1367
1757
|
], { signal: input.signal });
|
|
1368
1758
|
}
|
|
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
|
-
}
|
|
1382
1759
|
function isRetryableGeneratedSqlError(error) {
|
|
1383
1760
|
return !/\b(read-only|readonly|select or with|unsafe|delete|insert|update|drop|alter|create|attach|copy|pragma)\b/i.test(error);
|
|
1384
1761
|
}
|