@duckcodeailabs/dql-cli 1.12.3 → 1.12.4
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/assets/dql-notebook/assets/AgentAnswerCard-BJAnjaLA.js +5 -0
- package/dist/assets/dql-notebook/assets/{AgentLogPage-CsSC2tOR.js → AgentLogPage-J72WR6hk.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiBuildDialog-CjJ-9bQW.js → AiBuildDialog-CHJZfDmp.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiBuildResult-BvTcW9oE.js → AiBuildResult-Da7_Cn12.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AiSidePanel-CxFrOfCE.js → AiSidePanel-BY6W_5aC.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AnalyticsHome-BtXEdJNR.js → AnalyticsHome-u-buXgdv.js} +1 -1
- package/dist/assets/dql-notebook/assets/{AppsView-HjQkbbE6.js → AppsView-3k7ftDpd.js} +3 -3
- package/dist/assets/dql-notebook/assets/{BlockStudio-oQHTJsXC.js → BlockStudio-CLX4EUyO.js} +14 -14
- package/dist/assets/dql-notebook/assets/{BusinessArtifactView-Dpsftjs7.js → BusinessArtifactView-3THMNeyO.js} +1 -1
- package/dist/assets/dql-notebook/assets/{DbtFirstModelingPage-c6ZteaI6.js → DbtFirstModelingPage-BwBPwtdA.js} +1 -1
- package/dist/assets/dql-notebook/assets/{GitPage-Cj6mc-Yv.js → GitPage-CvEki0pe.js} +1 -1
- package/dist/assets/dql-notebook/assets/{GlobalAiRail-8G_rEyhK.js → GlobalAiRail-NXdk9kNy.js} +1 -1
- package/dist/assets/dql-notebook/assets/{GovernedContextPage-BTtzt0Za.js → GovernedContextPage-DILbw-Vr.js} +1 -1
- package/dist/assets/dql-notebook/assets/{HelpDocsPage-e2CesE73.js → HelpDocsPage-DgCu4bvY.js} +1 -1
- package/dist/assets/dql-notebook/assets/{HomePage-U3ryMLEF.js → HomePage-CK3fXTQo.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDAG-CEmgJzm3.js → LineageDAG-DW_NO3P2.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDetailView-CeV5QMXS.js → LineageDetailView-VT7DLJOB.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineageDrawer-B1oAn-N_.js → LineageDrawer-DSTm8aXw.js} +1 -1
- package/dist/assets/dql-notebook/assets/{LineagePathBreadcrumb-fP6RTXut.js → LineagePathBreadcrumb-DRlAYasJ.js} +1 -1
- package/dist/assets/dql-notebook/assets/{MiniLineageGraph-CBYSTmRC.js → MiniLineageGraph-CR_nhwhZ.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NewBlockModal-ksEwimjl.js → NewBlockModal-CJvjYiBF.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NewNotebookModal-CUx0WmXb.js → NewNotebookModal-BEpPH93n.js} +1 -1
- package/dist/assets/dql-notebook/assets/{NotebookEditor-BdxJL3LX.js → NotebookEditor-C7GDFwhF.js} +1 -1
- package/dist/assets/dql-notebook/assets/{ReadinessPage-DZ9wDW76.js → ReadinessPage-rZiOp2xr.js} +1 -1
- package/dist/assets/dql-notebook/assets/{SetupOnboarding-Bt0AAe0h.js → SetupOnboarding-BWjhTjPB.js} +1 -1
- package/dist/assets/dql-notebook/assets/{TrustBadge-DNVJK_mP.js → TrustBadge-RvZ9s6OQ.js} +1 -1
- package/dist/assets/dql-notebook/assets/{UnifiedAgentRunPanel-BNqE169i.js → UnifiedAgentRunPanel-CGPcpY1B.js} +18 -18
- package/dist/assets/dql-notebook/assets/{answer-to-notebook-B_gE6rsY.js → answer-to-notebook-Di227Cd4.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-left-DnKGhzOR.js → arrow-left-BZHDcWHR.js} +1 -1
- package/dist/assets/dql-notebook/assets/{arrow-right-CQ73DG7_.js → arrow-right-DlM1pC3_.js} +1 -1
- package/dist/assets/dql-notebook/assets/{book-open-text-CyBV5Nhw.js → book-open-text-DBcXFQAu.js} +1 -1
- package/dist/assets/dql-notebook/assets/{bot-UOPtBzCj.js → bot-BAywbvPm.js} +1 -1
- package/dist/assets/dql-notebook/assets/{chevron-up-CDHDU3N3.js → chevron-up-B_GfKE_F.js} +1 -1
- package/dist/assets/dql-notebook/assets/{circle-x-DC5nkL_u.js → circle-x-BcxtZ-ym.js} +1 -1
- package/dist/assets/dql-notebook/assets/{dagre.esm-C7ujXipB.js → dagre.esm-Bqs0lsEz.js} +1 -1
- package/dist/assets/dql-notebook/assets/{external-link-BiJ_dtTM.js → external-link-B24Dk9_t.js} +1 -1
- package/dist/assets/dql-notebook/assets/{grip-vertical-eXvkUerq.js → grip-vertical-D9g31N3j.js} +1 -1
- package/dist/assets/dql-notebook/assets/index-DU7j3Sqk.js +644 -0
- package/dist/assets/dql-notebook/assets/{link-2-DkHVJTuH.js → link-2-B36oR5Bj.js} +1 -1
- package/dist/assets/dql-notebook/assets/{list-tree-8lYW2Itb.js → list-tree-B9ZzGc4U.js} +1 -1
- package/dist/assets/dql-notebook/assets/{maximize-2-DjSovOdP.js → maximize-2-BrvQLPoj.js} +1 -1
- package/dist/assets/dql-notebook/assets/{minimize-2-DGlmQLES.js → minimize-2-BrpFmsxS.js} +1 -1
- package/dist/assets/dql-notebook/assets/{panel-right-open-DPc4gShM.js → panel-right-open-B1g1qhh5.js} +1 -1
- package/dist/assets/dql-notebook/assets/{pencil-nwaGtOdL.js → pencil-DwywbTqQ.js} +1 -1
- package/dist/assets/dql-notebook/assets/{rotate-ccw-ByjLhAu7.js → rotate-ccw-Db_KVs3J.js} +1 -1
- package/dist/assets/dql-notebook/assets/{semantic-fields-eEhwnQhG.js → semantic-fields-BIVuvP2l.js} +1 -1
- package/dist/assets/dql-notebook/assets/{triangle-alert-zws0cKhp.js → triangle-alert-Cne6C2aG.js} +1 -1
- package/dist/assets/dql-notebook/assets/usePersistedAgentThreadId-D6mtrJ-_.js +18 -0
- package/dist/assets/dql-notebook/assets/{user-round-CM8xNakq.js → user-round-DkzxN1xD.js} +1 -1
- package/dist/assets/dql-notebook/assets/{wand-sparkles-CIKc8r-4.js → wand-sparkles-CJUvFtur.js} +1 -1
- package/dist/assets/dql-notebook/assets/{workflow-6C0ZLbTi.js → workflow-BOvVTg7R.js} +1 -1
- package/dist/assets/dql-notebook/assets/{wrench-CeU7hF12.js → wrench-YeSpzZRt.js} +1 -1
- package/dist/assets/dql-notebook/assets/{x-DLWEzvri.js → x-C6tsHUR7.js} +1 -1
- package/dist/assets/dql-notebook/index.html +1 -1
- package/dist/block-invocation.js +4 -0
- package/dist/block-invocation.js.map +1 -1
- package/dist/llm/answer-loop-tools.d.ts +8 -2
- package/dist/llm/answer-loop-tools.d.ts.map +1 -1
- package/dist/llm/answer-loop-tools.js +29 -2
- package/dist/llm/answer-loop-tools.js.map +1 -1
- package/dist/llm/providers/dql-agent-provider.d.ts +27 -1
- package/dist/llm/providers/dql-agent-provider.d.ts.map +1 -1
- package/dist/llm/providers/dql-agent-provider.js +79 -15
- package/dist/llm/providers/dql-agent-provider.js.map +1 -1
- package/dist/llm/types.d.ts +8 -1
- package/dist/llm/types.d.ts.map +1 -1
- package/dist/local-runtime.d.ts +46 -0
- package/dist/local-runtime.d.ts.map +1 -1
- package/dist/local-runtime.js +606 -66
- package/dist/local-runtime.js.map +1 -1
- package/dist/package.json +10 -10
- package/dist/sql-result-filter.d.ts +39 -0
- package/dist/sql-result-filter.d.ts.map +1 -0
- package/dist/sql-result-filter.js +256 -0
- package/dist/sql-result-filter.js.map +1 -0
- package/package.json +10 -10
- package/dist/assets/dql-notebook/assets/AgentAnswerCard-BEIwKGm4.js +0 -5
- package/dist/assets/dql-notebook/assets/index-BMVfPaXr.js +0 -644
- package/dist/assets/dql-notebook/assets/usePersistedAgentThreadId-hxi1RZDm.js +0 -18
package/dist/local-runtime.js
CHANGED
|
@@ -20,7 +20,8 @@ import { fetchLatestPublishedDqlVersion, resolveDqlRuntimeVersionStatus } from '
|
|
|
20
20
|
import { resolveRetrievalHealthStatus } from './retrieval-health.js';
|
|
21
21
|
import { createDqlAgentProviderRunner, resolveAgentFollowUpContext } from './llm/providers/dql-agent-provider.js';
|
|
22
22
|
import { listRemoteMcpSettings, saveRemoteMcpSettings } from './llm/mcp-config.js';
|
|
23
|
-
import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, conversationHistoryFromContext, recallRelevantTurns, renderConversationEnvelopeForPrompt, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, composeSemanticQueryForQuestion, aggregationIntegrityIssuesForSql, buildLocalContextPack, applyContextPackCompatibility, toAgentRetrievalEvidence, prepareConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureAgentProjectReady, isAgentProjectIndexReady, currentMetadataFingerprint, ensureMetadataCatalogFresh, readIndexedDomainKnowledge, readIndexedKnowledge360, compactSemanticRuntimeFailure, classifyAnalyticalFailure, propose, proposePlan, recordGovernedCorrection, HintStore, defaultHintIndexPath, ensureHintIndexFresh, listHintsFromGit, getHintEvaluationFromGit, getCorrectionTraceFromGit, inspectGovernedHint, editGovernedHintCandidate, reopenGovernedHint, retireHint, supersedeHint, hintsConflict, mineJoinPatterns, reviewGovernedHint, AgentRunEngine, SqliteAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, buildDeterministicDashboardStory, synthesizeAnswer, streamOrGenerate, narrateResult, buildProposePreview, buildFromPrompt, internalRelationIdsInSql, defaultAgentRunStorePath, defaultAgentRunSqlitePath, resolveLocalOwner, resolveProposeConfig, recordQueryRun, recordRuntimeSchemaSnapshot, latestRuntimeSchemaSnapshotForProject, loadSkills, migrateLegacySkills, configuredSkillsPath, skillsDir, draftDomainSkillBootstrap, buildDomainSkillBootstrapPrompt, mergeDomainSkillBootstrapEnrichment, writeSkill, deleteSkill, deriveGeneratedDraftSlug, deriveAnalyticalRepair, reindexProject, invalidateAgentProjectState, recordAgentRuntimeVersion, resolveDomainContextEnvelope, projectEmbeddingProvider, isHashedEmbeddingProvider, clearProjectEmbeddingCache, upgradeVectorIndexForProject, openMetadataCatalog, defaultKgPath, planAppFromPrompt, KGStore, planResearch, loadSemanticMetrics, cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, routeReasoningEffort, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, upsertGeneratedDqlArtifactDraft, loadAgentSemanticLayer, } from '@duckcodeailabs/dql-agent';
|
|
23
|
+
import { ClaudeProvider, ConversationStore, advanceThreadState, buildConversationSnapshot, conversationHistoryFromContext, recallRelevantTurns, renderConversationEnvelopeForPrompt, GeminiProvider, MemoryStore, OllamaProvider, OpenAIProvider, buildBlockBusinessFingerprint, buildBlockSqlFingerprints, buildAnalysisQuestionPlan, composeSemanticQueryForQuestion, aggregationIntegrityIssuesForSql, buildLocalContextPack, applyContextPackCompatibility, toAgentRetrievalEvidence, prepareConversationPath, defaultMemoryPath, ensureDefaultMemoryFiles, ensureAgentProjectReady, isAgentProjectIndexReady, currentMetadataFingerprint, ensureMetadataCatalogFresh, readIndexedDomainKnowledge, readIndexedKnowledge360, compactSemanticRuntimeFailure, classifyAnalyticalFailure, propose, proposePlan, recordGovernedCorrection, HintStore, defaultHintIndexPath, ensureHintIndexFresh, listHintsFromGit, getHintEvaluationFromGit, getCorrectionTraceFromGit, inspectGovernedHint, editGovernedHintCandidate, reopenGovernedHint, retireHint, supersedeHint, hintsConflict, mineJoinPatterns, reviewGovernedHint, AgentRunEngine, SqliteAgentRunStore, defaultAgentRunGates, createLlmAgentRunPlanner, createHybridRouter, computeResultStats, buildDeterministicDashboardStory, synthesizeAnswer, streamOrGenerate, narrateResult, buildProposePreview, buildFromPrompt, internalRelationIdsInSql, defaultAgentRunStorePath, defaultAgentRunSqlitePath, resolveLocalOwner, resolveProposeConfig, recordQueryRun, recordRuntimeSchemaSnapshot, latestRuntimeSchemaSnapshotForProject, loadSkills, migrateLegacySkills, configuredSkillsPath, skillsDir, draftDomainSkillBootstrap, buildDomainSkillBootstrapPrompt, mergeDomainSkillBootstrapEnrichment, writeSkill, deleteSkill, deriveGeneratedDraftSlug, deriveAnalyticalRepair, reindexProject, invalidateAgentProjectState, recordAgentRuntimeVersion, resolveDomainContextEnvelope, projectEmbeddingProvider, isHashedEmbeddingProvider, clearProjectEmbeddingCache, upgradeVectorIndexForProject, openMetadataCatalog, defaultKgPath, planAppFromPrompt, KGStore, planResearch, loadSemanticMetrics, cascadeTraceToEvidenceRouteSteps, createCascadeAnswerResult, createCascadeTrace, routeReasoningEffort, routeForCascadeAnswerTier, clampReasoningEffort, bumpReasoningEffort, resolveThinkingMode, coerceThinkingMode, upsertGeneratedDqlArtifactDraft, loadAgentSemanticLayer, isTrustedConversationTurn, analyticalError, withAnalyticalErrorOrigin, withAnalyticalErrorOriginSync, } from '@duckcodeailabs/dql-agent';
|
|
24
|
+
import { addSqlResultFilter, filterableResultColumns, replaceBlockStudioSql } from './sql-result-filter.js';
|
|
24
25
|
import { gatherProposeEnrichment } from './propose-enrich.js';
|
|
25
26
|
import { handleAppsApi, proposeAppAiBuild, recommendVisualization } from './apps-api.js';
|
|
26
27
|
import { getActiveProvider, getEffectiveProviderConfig, isProviderSettingsId, listProviderSettings, saveProviderSettings, } from './settings/provider-settings.js';
|
|
@@ -435,6 +436,9 @@ async function conversationContextFromThread(store, threadId, clientContext, que
|
|
|
435
436
|
trustLabel: turn.trustLabel,
|
|
436
437
|
runStatus: turn.runStatus,
|
|
437
438
|
stopReason: turn.stopReason,
|
|
439
|
+
// Carried so every consumer can compute trust from the same fields.
|
|
440
|
+
refusalCode: turn.refusalCode,
|
|
441
|
+
executionError: turn.executionError,
|
|
438
442
|
certification: turn.certification,
|
|
439
443
|
contextPackId: turn.contextPackId,
|
|
440
444
|
dqlArtifact: turn.dqlArtifact,
|
|
@@ -473,9 +477,30 @@ async function conversationContextFromThread(store, threadId, clientContext, que
|
|
|
473
477
|
...(serverSnapshot ? { conversationEnvelope: serverSnapshot } : {}),
|
|
474
478
|
...(serverSnapshot ? { serverSnapshot } : {}),
|
|
475
479
|
...(thread?.rollingSummary ? { conversationSummary: thread.rollingSummary } : {}),
|
|
476
|
-
|
|
480
|
+
// `activeTurnId` is the FOLLOW-UP ANCHOR: the turn whose filters, prior DQL
|
|
481
|
+
// artifact, and source SQL the next question builds on. Anchoring it to the
|
|
482
|
+
// last turn unconditionally meant a failed turn became the anchor, and its
|
|
483
|
+
// broken artifact and failing SQL were handed to the next question as
|
|
484
|
+
// authoritative context — the sticky-error loop. `latestTurnId` keeps the
|
|
485
|
+
// real tail for presentation.
|
|
486
|
+
...(turns.length > 0
|
|
487
|
+
? {
|
|
488
|
+
turns,
|
|
489
|
+
activeTurnId: lastTrustedTurnId(turns) ?? undefined,
|
|
490
|
+
latestTurnId: turns[turns.length - 1].id,
|
|
491
|
+
}
|
|
492
|
+
: {}),
|
|
477
493
|
};
|
|
478
494
|
}
|
|
495
|
+
/** The most recent turn a follow-up may safely build on. */
|
|
496
|
+
function lastTrustedTurnId(turns) {
|
|
497
|
+
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
|
498
|
+
const turn = turns[index];
|
|
499
|
+
if (isTrustedConversationTurn(turn))
|
|
500
|
+
return turn.id;
|
|
501
|
+
}
|
|
502
|
+
return undefined;
|
|
503
|
+
}
|
|
479
504
|
/** Best-effort: persist a completed run as a conversation turn (never throws). */
|
|
480
505
|
function recordConversationTurn(store, threadId, run) {
|
|
481
506
|
if (!store || !threadId)
|
|
@@ -489,8 +514,12 @@ function recordConversationTurn(store, threadId, run) {
|
|
|
489
514
|
// turn folding its (possibly misparsed) filters and measures into working
|
|
490
515
|
// state poisoned every subsequent ask: the SAME question that worked once
|
|
491
516
|
// then failed forever because the bad turn's phantom state carried forward.
|
|
492
|
-
|
|
493
|
-
|
|
517
|
+
//
|
|
518
|
+
// The previous guard tested `run.status`, which cannot express this: a
|
|
519
|
+
// grounding-gap refusal and a perfectly good uncertified answer are BOTH
|
|
520
|
+
// `needs_review`, so the guard admitted exactly the failures its comment
|
|
521
|
+
// was written to exclude. Trust is now derived from answer state.
|
|
522
|
+
if (isTrustedConversationTurn(turn))
|
|
494
523
|
advanceThreadState(store, threadId, turn);
|
|
495
524
|
}
|
|
496
525
|
catch {
|
|
@@ -529,6 +558,11 @@ export function conversationTurnInputFromRun(run) {
|
|
|
529
558
|
trustLabel: agentRunString(payload?.trustLabel) ?? run.trustState,
|
|
530
559
|
runStatus: run.status,
|
|
531
560
|
stopReason: run.stopReason,
|
|
561
|
+
// Persisted so a later turn can tell a refusal from an answer. `runStatus`
|
|
562
|
+
// alone cannot: a grounding gap and a good uncertified answer are both
|
|
563
|
+
// `needs_review`.
|
|
564
|
+
refusalCode: agentRunString(payload?.refusalCode),
|
|
565
|
+
executionError: agentRunString(payload?.executionError),
|
|
532
566
|
certification: agentRunString(payload?.certification),
|
|
533
567
|
sourceCertifiedBlock: agentRunString(payload?.sourceCertifiedBlock)
|
|
534
568
|
?? (artifact?.kind === 'answer' ? agentRunString(artifact.ref) : undefined),
|
|
@@ -1542,6 +1576,7 @@ export async function startLocalServer(opts) {
|
|
|
1542
1576
|
getSchemaContext: (question, preparedContextPack) => getSchemaContextForAgent(question, preparedContextPack, semanticConnection, request.executionTarget?.target === 'connection'
|
|
1543
1577
|
? request.executionTarget.connectionName
|
|
1544
1578
|
: undefined),
|
|
1579
|
+
probeNamedRelations: (relations) => probeNamedRelationsForAgent(relations, semanticConnection),
|
|
1545
1580
|
}, (turn) => {
|
|
1546
1581
|
if (turn.kind === 'thinking')
|
|
1547
1582
|
onProgress?.(turn.text);
|
|
@@ -1927,7 +1962,7 @@ export async function startLocalServer(opts) {
|
|
|
1927
1962
|
],
|
|
1928
1963
|
};
|
|
1929
1964
|
}
|
|
1930
|
-
const
|
|
1965
|
+
const answeredRoute = resolvedRunRouteFromAnswer(governedAnswer) ?? route;
|
|
1931
1966
|
const isCertified = governedAnswer.certification === 'certified' || governedAnswer.kind === 'certified';
|
|
1932
1967
|
const isSemantic = governedAnswer.route?.tier === 'semantic_metric';
|
|
1933
1968
|
const requestedNotebookDataset = findMentionedNotebookDataset(request.question, datasetWorkspace.list());
|
|
@@ -1958,6 +1993,12 @@ export async function startLocalServer(opts) {
|
|
|
1958
1993
|
// provider outages are blocked so the UI can offer an explicit retry.
|
|
1959
1994
|
const needsClarification = governedAnswer.kind === 'no_answer'
|
|
1960
1995
|
&& !isGroundingGap && !isProviderError && !isModelDeclined && !isPolicyBlocked;
|
|
1996
|
+
// A run that ASKED THE USER A QUESTION is a clarify turn, whatever route it
|
|
1997
|
+
// would have taken had it answered. Persisting the resolved analytical route
|
|
1998
|
+
// instead (`generated_answer`) meant the engine's clarification-continuation
|
|
1999
|
+
// lookup — which matches on `route === 'clarify'` — never found it, so the
|
|
2000
|
+
// reply ran context-free and was clarified again: the reported clarify loop.
|
|
2001
|
+
const resolvedRoute = needsClarification ? 'clarify' : answeredRoute;
|
|
1961
2002
|
const sql = governedAnswer.proposedSql ?? governedAnswer.sql;
|
|
1962
2003
|
const runnableSql = governedAnswer.kind === 'no_answer' || isExecutionFailure || (isExploratory && !governedAnswer.result)
|
|
1963
2004
|
? undefined
|
|
@@ -2002,31 +2043,45 @@ export async function startLocalServer(opts) {
|
|
|
2002
2043
|
synthesizedAnswer = undefined;
|
|
2003
2044
|
}
|
|
2004
2045
|
}
|
|
2046
|
+
// A grounding/modeling gap is a REFUSAL. It was computed above but never
|
|
2047
|
+
// reached this ternary, so it fell through to `needs_review` — the same
|
|
2048
|
+
// status as a perfectly good uncertified answer. Every downstream trust
|
|
2049
|
+
// check then treated the refusal as a usable turn, which is how one failed
|
|
2050
|
+
// answer poisoned every later question in the thread.
|
|
2005
2051
|
const status = isTerminalFailure
|
|
2006
2052
|
? 'blocked'
|
|
2007
2053
|
: needsClarification
|
|
2008
2054
|
? 'needs_clarification'
|
|
2009
|
-
:
|
|
2010
|
-
? '
|
|
2011
|
-
:
|
|
2055
|
+
: isGroundingGap || isModelDeclined
|
|
2056
|
+
? 'blocked'
|
|
2057
|
+
: isCertified || isSemantic
|
|
2058
|
+
? 'completed'
|
|
2059
|
+
: 'needs_review';
|
|
2012
2060
|
const trustState = isTerminalFailure
|
|
2013
2061
|
? 'blocked'
|
|
2014
2062
|
: needsClarification
|
|
2015
2063
|
? 'not_applicable'
|
|
2016
|
-
:
|
|
2017
|
-
? '
|
|
2018
|
-
:
|
|
2019
|
-
? '
|
|
2020
|
-
:
|
|
2064
|
+
: isGroundingGap || isModelDeclined
|
|
2065
|
+
? 'blocked'
|
|
2066
|
+
: isCertified
|
|
2067
|
+
? 'certified'
|
|
2068
|
+
: isSemantic
|
|
2069
|
+
? 'governed'
|
|
2070
|
+
: 'review_required';
|
|
2021
2071
|
const stopReason = isTerminalFailure
|
|
2022
2072
|
? 'blocked'
|
|
2023
2073
|
: needsClarification
|
|
2024
2074
|
? 'needs_clarification'
|
|
2025
|
-
:
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2075
|
+
// A gap is terminal, but it is NOT a provider outage: it still owes the
|
|
2076
|
+
// user its evidence trace, and its next-actions below stay the
|
|
2077
|
+
// grounding-gap set rather than "retry the provider".
|
|
2078
|
+
: isGroundingGap || isModelDeclined
|
|
2079
|
+
? 'human_review_required'
|
|
2080
|
+
: isCertified
|
|
2081
|
+
? 'certified_answer_found'
|
|
2082
|
+
: isSemantic
|
|
2083
|
+
? 'governed_semantic_answer'
|
|
2084
|
+
: 'human_review_required';
|
|
2030
2085
|
const terminalFailureActions = governedAnswer.analyticalFailure
|
|
2031
2086
|
? governedAnswer.analyticalFailure.code === 'PERMISSION_DENIED'
|
|
2032
2087
|
? [{ id: 'repair-access', label: 'Change connection or request access', route: 'blocked' }]
|
|
@@ -2172,33 +2227,43 @@ export async function startLocalServer(opts) {
|
|
|
2172
2227
|
const kind = routeDecision?.conversationalKind ?? 'smalltalk';
|
|
2173
2228
|
const isGeneralKnowledge = routeDecision?.category === 'general_knowledge';
|
|
2174
2229
|
const answerKind = isGeneralKnowledge ? 'general_knowledge' : 'conversational';
|
|
2175
|
-
const catalogContext = buildAgentRunCatalogContext();
|
|
2230
|
+
const catalogContext = kind === 'answer_explanation' ? '' : buildAgentRunCatalogContext();
|
|
2176
2231
|
const suggestions = buildConversationSuggestions(projectRoot, kind);
|
|
2177
2232
|
const nextActions = suggestions.map((prompt, index) => ({
|
|
2178
2233
|
id: `suggest-question-${index + 1}`,
|
|
2179
2234
|
label: prompt,
|
|
2180
2235
|
}));
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
...(conversationMemory ? [{ role: 'system', content: conversationMemory }] : []),
|
|
2190
|
-
...(request.history ?? []).slice(-6).map((message) => ({ role: message.role, content: message.text })),
|
|
2191
|
-
{ role: 'user', content: request.question },
|
|
2192
|
-
];
|
|
2193
|
-
text = (await provider.generate(messages, { maxTokens: 320, temperature: 0.6 })).trim();
|
|
2194
|
-
// Perceived-latency: surface the reply as one delta for surfaces wired to stream.
|
|
2195
|
-
if (text)
|
|
2196
|
-
emitAnswerDelta?.(text);
|
|
2197
|
-
}
|
|
2236
|
+
// A question about the latest answer is an evidence lookup, not another
|
|
2237
|
+
// warehouse run. Resolve it from the persisted artifact contract before
|
|
2238
|
+
// considering a provider-generated conversational reply.
|
|
2239
|
+
let text = kind === 'answer_explanation'
|
|
2240
|
+
? buildPriorAnswerExplanation(request.question, request.conversationContext)
|
|
2241
|
+
: undefined;
|
|
2242
|
+
if (text) {
|
|
2243
|
+
emitAnswerDelta?.(text);
|
|
2198
2244
|
}
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2245
|
+
else {
|
|
2246
|
+
try {
|
|
2247
|
+
const provider = await createBlockStudioAssistProvider(projectRoot);
|
|
2248
|
+
if (provider) {
|
|
2249
|
+
const system = buildConversationSystemPrompt(kind, isGeneralKnowledge, catalogContext, request.audience ?? 'analyst');
|
|
2250
|
+
const conversationMemory = renderConversationMemoryForPrompt(request.conversationContext);
|
|
2251
|
+
const messages = [
|
|
2252
|
+
{ role: 'system', content: system },
|
|
2253
|
+
...(conversationMemory ? [{ role: 'system', content: conversationMemory }] : []),
|
|
2254
|
+
...(request.history ?? []).slice(-6).map((message) => ({ role: message.role, content: message.text })),
|
|
2255
|
+
{ role: 'user', content: request.question },
|
|
2256
|
+
];
|
|
2257
|
+
text = (await provider.generate(messages, { maxTokens: 320, temperature: 0.6 })).trim();
|
|
2258
|
+
// Perceived-latency: surface the reply as one delta for surfaces wired to stream.
|
|
2259
|
+
if (text)
|
|
2260
|
+
emitAnswerDelta?.(text);
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
catch {
|
|
2264
|
+
// Provider unavailable / errored — fall through to the deterministic reply.
|
|
2265
|
+
text = undefined;
|
|
2266
|
+
}
|
|
2202
2267
|
}
|
|
2203
2268
|
if (!text) {
|
|
2204
2269
|
text = buildConversationContextRecap(request.conversationContext)
|
|
@@ -2206,7 +2271,7 @@ export async function startLocalServer(opts) {
|
|
|
2206
2271
|
emitAnswerDelta?.(text);
|
|
2207
2272
|
}
|
|
2208
2273
|
return {
|
|
2209
|
-
summary: 'Replied conversationally.',
|
|
2274
|
+
summary: kind === 'answer_explanation' ? 'Explained the previous answer.' : 'Replied conversationally.',
|
|
2210
2275
|
answer: text,
|
|
2211
2276
|
answerKind,
|
|
2212
2277
|
status: 'completed',
|
|
@@ -2715,6 +2780,9 @@ export async function startLocalServer(opts) {
|
|
|
2715
2780
|
const serverSnapshot = agentRunRecord(request.conversationContext?.conversationEnvelope
|
|
2716
2781
|
?? request.conversationContext?.serverSnapshot);
|
|
2717
2782
|
const topicRelation = agentRunString(serverSnapshot?.topicRelation);
|
|
2783
|
+
const workspaceSurface = agentRunWorkspaceValue(request, 'surface');
|
|
2784
|
+
const blockAuthoringContext = request.requestedMode === 'block'
|
|
2785
|
+
|| workspaceSurface === 'block-studio';
|
|
2718
2786
|
// The readiness marker is source-versioned. When it matches, pass the
|
|
2719
2787
|
// already-built metadata identity into retrieval so buildLocalContextPack
|
|
2720
2788
|
// opens the immutable snapshot directly instead of rebuilding all metadata
|
|
@@ -2737,6 +2805,8 @@ export async function startLocalServer(opts) {
|
|
|
2737
2805
|
const task = buildLocalContextPack(projectRoot, {
|
|
2738
2806
|
question: request.question,
|
|
2739
2807
|
focusObjectKey: request.selectedEvidenceId,
|
|
2808
|
+
mode: blockAuthoringContext ? 'build' : 'question',
|
|
2809
|
+
includeDraftBlocks: blockAuthoringContext,
|
|
2740
2810
|
followUp,
|
|
2741
2811
|
priorContextPackId: agentRunString(request.conversationContext?.contextPackId),
|
|
2742
2812
|
conversationTopicRelation: topicRelation === 'continuation'
|
|
@@ -2750,7 +2820,9 @@ export async function startLocalServer(opts) {
|
|
|
2750
2820
|
reusePolicy: 'seed',
|
|
2751
2821
|
preparedMetadataFingerprint,
|
|
2752
2822
|
...(runtimeSchemaSnapshot ? { runtimeSchemaSnapshot } : {}),
|
|
2753
|
-
surface:
|
|
2823
|
+
surface: blockAuthoringContext
|
|
2824
|
+
? 'block'
|
|
2825
|
+
: workspaceSurface ?? 'notebook',
|
|
2754
2826
|
selectedContext: {
|
|
2755
2827
|
selectedObject: request.selectedObject,
|
|
2756
2828
|
workspaceContext: request.workspaceContext,
|
|
@@ -3021,43 +3093,55 @@ export async function startLocalServer(opts) {
|
|
|
3021
3093
|
});
|
|
3022
3094
|
};
|
|
3023
3095
|
const executeDqlArtifactSourceForAgent = async (source, metadata = {}, invocationInput, executionConnection, executionConnectionName) => {
|
|
3024
|
-
|
|
3096
|
+
// Everything from here to the executor call is DQL's OWN block compiler:
|
|
3097
|
+
// parse, parameter resolution, plan build, semantic compose. A failure in
|
|
3098
|
+
// any of it means DQL could not rebuild its artifact — the warehouse was
|
|
3099
|
+
// never contacted. Tagging the origin here stops the answer loop from
|
|
3100
|
+
// reporting these as "the generated SQL failed to execute".
|
|
3101
|
+
const invocation = withAnalyticalCompilationOrigin('bind', () => prepareBlockInvocation({
|
|
3025
3102
|
source,
|
|
3026
3103
|
parameters: invocationInput?.parameters,
|
|
3027
3104
|
parameterSources: invocationInput?.parameterSources,
|
|
3028
3105
|
question: invocationInput?.question,
|
|
3029
3106
|
surface: 'ask_ai',
|
|
3030
|
-
});
|
|
3031
|
-
if (invocation.errors.length > 0)
|
|
3032
|
-
throw
|
|
3107
|
+
}));
|
|
3108
|
+
if (invocation.errors.length > 0) {
|
|
3109
|
+
throw analyticalError(invocation.errors.join(' '), { origin: 'dql_compilation', stage: 'compile' });
|
|
3110
|
+
}
|
|
3033
3111
|
if (invocation.unresolvedParameters.length > 0) {
|
|
3034
|
-
throw
|
|
3112
|
+
throw analyticalError(`I need values for: ${invocation.unresolvedParameters.join(', ')}.`, {
|
|
3113
|
+
origin: 'dql_compilation',
|
|
3114
|
+
stage: 'bind',
|
|
3115
|
+
code: 'unresolved_parameter',
|
|
3116
|
+
offending: { parameter: invocation.unresolvedParameters[0] },
|
|
3117
|
+
});
|
|
3035
3118
|
}
|
|
3036
3119
|
const activeConnection = requireActiveConnection(executionConnection);
|
|
3037
3120
|
const precompiledSemanticQuery = Boolean(extractBlockStudioSql(source));
|
|
3038
3121
|
const tableMapping = !precompiledSemanticQuery
|
|
3039
3122
|
? await resolveSemanticTableMapping(executor, activeConnection, semanticLayer, projectRoot, executionConnectionName)
|
|
3040
3123
|
: undefined;
|
|
3041
|
-
const
|
|
3042
|
-
|
|
3124
|
+
const composeLayer = semanticLayer;
|
|
3125
|
+
const semanticCompose = composeLayer
|
|
3126
|
+
? await withAnalyticalErrorOrigin({ origin: 'dql_compilation', stage: 'compile' }, () => composeSemanticBlockSqlForRuntime(source, composeLayer, {
|
|
3043
3127
|
driver: activeConnection.driver,
|
|
3044
3128
|
tableMapping,
|
|
3045
3129
|
projectRoot,
|
|
3046
3130
|
projectConfig,
|
|
3047
3131
|
detectedProvider: semanticDetectedProvider,
|
|
3048
3132
|
parameters: invocation.values,
|
|
3049
|
-
})
|
|
3133
|
+
}))
|
|
3050
3134
|
: null;
|
|
3051
|
-
const plan = buildExecutionPlan({ id: `agent-${metadata.name ?? 'dql-artifact'}`, type: 'dql', source, title: metadata.name ?? 'DQL artifact' }, {
|
|
3135
|
+
const plan = withAnalyticalCompilationOrigin('compile', () => buildExecutionPlan({ id: `agent-${metadata.name ?? 'dql-artifact'}`, type: 'dql', source, title: metadata.name ?? 'DQL artifact' }, {
|
|
3052
3136
|
semanticLayer,
|
|
3053
3137
|
driver: activeConnection.driver,
|
|
3054
3138
|
tableMapping,
|
|
3055
3139
|
parameters: invocation.values,
|
|
3056
3140
|
semanticSql: semanticCompose?.sql ?? undefined,
|
|
3057
|
-
});
|
|
3141
|
+
}));
|
|
3058
3142
|
if (!plan && !semanticCompose?.sql) {
|
|
3059
3143
|
const semanticError = semanticCompose?.diagnostics.find((diagnostic) => diagnostic.severity === 'error')?.message;
|
|
3060
|
-
throw
|
|
3144
|
+
throw analyticalError(semanticError ?? `DQL artifact "${metadata.name ?? 'draft'}" produced no executable SQL.`, { origin: 'dql_compilation', stage: 'compile' });
|
|
3061
3145
|
}
|
|
3062
3146
|
const prepared = prepareLocalExecution(semanticCompose?.sql ?? plan.sql, activeConnection, projectRoot, projectConfig);
|
|
3063
3147
|
const executionSql = invocationInput?.rowLimit
|
|
@@ -3176,9 +3260,123 @@ export async function startLocalServer(opts) {
|
|
|
3176
3260
|
},
|
|
3177
3261
|
};
|
|
3178
3262
|
};
|
|
3263
|
+
/**
|
|
3264
|
+
* Build the reusable DQL artifact for an answer that HAS ALREADY EXECUTED,
|
|
3265
|
+
* and check that it recompiles.
|
|
3266
|
+
*
|
|
3267
|
+
* AGT-010 requires the attached artifact to be the one used for execution, so
|
|
3268
|
+
* that "Apply" cannot diverge from what Ask displayed. The old code satisfied
|
|
3269
|
+
* that invariant backwards: it made EXECUTION follow the artifact, routing the
|
|
3270
|
+
* model's SQL through `parameterizeSqlForDqlImport` → `candidateToDqlSource` →
|
|
3271
|
+
* the DQL parser → `processSQL` before running anything. Every one of those
|
|
3272
|
+
* stages could fail on SQL the warehouse would have accepted, and each failure
|
|
3273
|
+
* destroyed the whole answer.
|
|
3274
|
+
*
|
|
3275
|
+
* Now the executed string is the source of truth and the artifact is derived
|
|
3276
|
+
* from it. Building the artifact is a SAVE concern, so a failure here degrades
|
|
3277
|
+
* the artifact to review-required and never throws: the user still gets their
|
|
3278
|
+
* answer, and only "Save as block" is affected.
|
|
3279
|
+
*/
|
|
3280
|
+
const buildVerifiedGeneratedArtifact = (question, sql, executedSql, seed, warnings, resultColumns = [], dialect = 'duckdb') => {
|
|
3281
|
+
// Which result columns the user may promote to a filter input. Decided here
|
|
3282
|
+
// because it needs a real dialect parse of the SQL that actually ran.
|
|
3283
|
+
const filterableColumns = resultColumns.length > 0
|
|
3284
|
+
? filterableResultColumns(executedSql, resultColumns, dialect)
|
|
3285
|
+
: [];
|
|
3286
|
+
const withCandidates = (artifact) => (filterableColumns.length > 0 ? { ...artifact, filterableColumns } : artifact);
|
|
3287
|
+
let artifact;
|
|
3288
|
+
try {
|
|
3289
|
+
artifact = generatedSqlArtifactContract(question, sql, seed);
|
|
3290
|
+
}
|
|
3291
|
+
catch (error) {
|
|
3292
|
+
warnings.push(`This answer could not be captured as a reusable DQL block: ${apiErrorMessage(error)}`);
|
|
3293
|
+
return undefined;
|
|
3294
|
+
}
|
|
3295
|
+
try {
|
|
3296
|
+
const invocation = prepareBlockInvocation({ source: artifact.source, question, surface: 'ask_ai' });
|
|
3297
|
+
const divergence = invocation.errors.length > 0
|
|
3298
|
+
? invocation.errors.join(' ')
|
|
3299
|
+
: invocation.unresolvedParameters.length > 0
|
|
3300
|
+
? `it still needs values for ${invocation.unresolvedParameters.join(', ')}`
|
|
3301
|
+
: undefined;
|
|
3302
|
+
if (divergence) {
|
|
3303
|
+
warnings.push(`The reusable DQL block for this answer needs review before reuse: ${divergence}`);
|
|
3304
|
+
// Keep the declared parameter contract even when the block cannot run
|
|
3305
|
+
// as-is. This branch is precisely "it still needs values for X", so the
|
|
3306
|
+
// parameter-input surface needs the list in order to ASK for them —
|
|
3307
|
+
// dropping it would leave the user a review-required block with no way
|
|
3308
|
+
// to see, or supply, what it wants.
|
|
3309
|
+
return withCandidates({
|
|
3310
|
+
...artifact,
|
|
3311
|
+
trustState: 'review_required',
|
|
3312
|
+
compiledSql: executedSql,
|
|
3313
|
+
...(invocation.parameters.length > 0 ? { parameters: invocation.parameters } : {}),
|
|
3314
|
+
...(Object.keys(invocation.values).length > 0 ? { parameterValues: invocation.values } : {}),
|
|
3315
|
+
});
|
|
3316
|
+
}
|
|
3317
|
+
return withCandidates({
|
|
3318
|
+
...artifact,
|
|
3319
|
+
compiledSql: executedSql,
|
|
3320
|
+
...(invocation.parameters.length > 0 ? { parameters: invocation.parameters } : {}),
|
|
3321
|
+
...(Object.keys(invocation.values).length > 0 ? { parameterValues: invocation.values } : {}),
|
|
3322
|
+
});
|
|
3323
|
+
}
|
|
3324
|
+
catch (error) {
|
|
3325
|
+
warnings.push(`The reusable DQL block for this answer needs review before reuse: ${apiErrorMessage(error)}`);
|
|
3326
|
+
return withCandidates({ ...artifact, trustState: 'review_required', compiledSql: executedSql });
|
|
3327
|
+
}
|
|
3328
|
+
};
|
|
3329
|
+
/**
|
|
3330
|
+
* Execute the model's SQL EXACTLY as validated — the same way a notebook cell
|
|
3331
|
+
* runs it. This is the fix for "the query Ask generated fails in Ask but runs
|
|
3332
|
+
* in the notebook": both paths now reach `executor.executeQuery` with the same
|
|
3333
|
+
* string, so the only remaining reasons to differ are the connection and the
|
|
3334
|
+
* governance gates, both of which report themselves honestly.
|
|
3335
|
+
*/
|
|
3336
|
+
const executeGeneratedSqlDirect = async (question, sql, seed, executionConnection) => {
|
|
3337
|
+
const activeConnection = requireActiveConnection(executionConnection);
|
|
3338
|
+
const rowBound = clampAnalyticalRowBound(seed?.limit ?? 200);
|
|
3339
|
+
const trimmed = sql.trim().replace(/;\s*$/, '').trim();
|
|
3340
|
+
if (!trimmed)
|
|
3341
|
+
throw analyticalError('The generated SQL was empty.', { origin: 'host', stage: 'execute' });
|
|
3342
|
+
const readOnlyError = readOnlySqlValidationError(trimmed, 'Generated SQL', activeConnection.driver);
|
|
3343
|
+
if (readOnlyError) {
|
|
3344
|
+
throw analyticalError(readOnlyError, { origin: 'governance_gate', stage: 'validation', code: 'unsafe_sql' });
|
|
3345
|
+
}
|
|
3346
|
+
const prepared = prepareLocalExecution(trimmed, activeConnection, projectRoot, projectConfig);
|
|
3347
|
+
const bounded = buildRowBoundedSql(prepared.sql, rowBound, activeConnection.driver);
|
|
3348
|
+
const app = loadRuntimeApp(projectRoot, activePersonaAppId());
|
|
3349
|
+
const sourceDomain = seed?.source?.match(/\bdomain\s*=\s*"([^"]+)"/i)?.[1];
|
|
3350
|
+
assertAppAccess({ app, domain: sourceDomain ?? app?.domain, level: 'execute' });
|
|
3351
|
+
const rawResult = await executor.executeQuery(bounded.sql, [], runtimeVariables({}), prepared.connection);
|
|
3352
|
+
const normalized = normalizeQueryResult(rawResult);
|
|
3353
|
+
// The authoritative bound. `buildRowBoundedSql` may legitimately decline to
|
|
3354
|
+
// touch the statement, and only one of 15 drivers honours a maxRows hint, so
|
|
3355
|
+
// the host is the only layer correctness may depend on.
|
|
3356
|
+
const truncated = normalized.rows.length > rowBound;
|
|
3357
|
+
const rows = truncated ? normalized.rows.slice(0, rowBound) : normalized.rows;
|
|
3358
|
+
const warnings = [];
|
|
3359
|
+
const artifact = buildVerifiedGeneratedArtifact(question, trimmed, bounded.sql, seed, warnings, normalized.columns, activeConnection.driver);
|
|
3360
|
+
return {
|
|
3361
|
+
columns: normalized.columns,
|
|
3362
|
+
rows,
|
|
3363
|
+
rowCount: rows.length,
|
|
3364
|
+
executionTime: normalized.executionTime,
|
|
3365
|
+
sql: bounded.sql,
|
|
3366
|
+
...(truncated ? { truncated: true } : {}),
|
|
3367
|
+
...(artifact ? { dqlArtifact: { ...artifact, limit: rowBound } } : {}),
|
|
3368
|
+
...(warnings.length > 0 ? { validationWarnings: warnings } : {}),
|
|
3369
|
+
executionReceipt: createDqlArtifactExecutionReceipt(artifact?.source ?? trimmed, bounded.sql, {}, { columns: normalized.columns, rows, rowCount: rows.length }),
|
|
3370
|
+
};
|
|
3371
|
+
};
|
|
3179
3372
|
const executeGeneratedArtifactForAgent = async (question, sql, seed, executionConnection, executionConnectionName) => {
|
|
3180
|
-
|
|
3181
|
-
|
|
3373
|
+
// A seed that is already a certified/saved artifact keeps the DQL-first
|
|
3374
|
+
// path: there the `.dql` source IS the contract, and its parameters and
|
|
3375
|
+
// semantic refs must be compiled, not bypassed.
|
|
3376
|
+
if (seed && seed.kind !== 'sql_block') {
|
|
3377
|
+
return executeArtifactReferenceForAgent({ ...seed, limit: seed.limit ?? 200 }, question, executionConnection, executionConnectionName);
|
|
3378
|
+
}
|
|
3379
|
+
return executeGeneratedSqlDirect(question, sql, seed, executionConnection);
|
|
3182
3380
|
};
|
|
3183
3381
|
/**
|
|
3184
3382
|
* EXP-001: execution host for the deliberately narrow non-governed lane.
|
|
@@ -3287,6 +3485,60 @@ export async function startLocalServer(opts) {
|
|
|
3287
3485
|
return { proofs, sql: boundedSql, repairs, error: error instanceof Error ? error.message : String(error) };
|
|
3288
3486
|
}
|
|
3289
3487
|
};
|
|
3488
|
+
/**
|
|
3489
|
+
* Ask the warehouse whether specific named relations exist, AFTER the cached
|
|
3490
|
+
* catalog has already missed them.
|
|
3491
|
+
*
|
|
3492
|
+
* `getSchemaContextForAgent` deliberately returns nothing on a lexical miss —
|
|
3493
|
+
* "a lexical miss is not permission to rescan every visible schema". That is
|
|
3494
|
+
* right for BULK retrieval, but it also meant a relation the model named
|
|
3495
|
+
* explicitly, and that the notebook queries without complaint, was refused as
|
|
3496
|
+
* "outside the inspected metadata context" without the warehouse ever being
|
|
3497
|
+
* asked. A point lookup of up to four named `(schema, table)` pairs is not a
|
|
3498
|
+
* rescan.
|
|
3499
|
+
*
|
|
3500
|
+
* The payoff is honesty as much as recall: after this probe, a miss is
|
|
3501
|
+
* evidence. The refusal can say the relation is not visible to this
|
|
3502
|
+
* connection, rather than only that DQL had not looked at it.
|
|
3503
|
+
*/
|
|
3504
|
+
const probeNamedRelationsForAgent = async (relations, probeConnection) => {
|
|
3505
|
+
const probeTarget = probeConnection ?? connection;
|
|
3506
|
+
if (!probeTarget)
|
|
3507
|
+
return undefined;
|
|
3508
|
+
// Same operator switch that governs live value grounding: a deployment that
|
|
3509
|
+
// wants zero live metadata traffic keeps it.
|
|
3510
|
+
if (resolveAgentRuntimeValueGrounding(projectConfig).mode !== 'safe_automatic')
|
|
3511
|
+
return undefined;
|
|
3512
|
+
const sql = buildNamedRelationProbeSql(relations);
|
|
3513
|
+
if (!sql)
|
|
3514
|
+
return undefined;
|
|
3515
|
+
const prepared = prepareLocalExecution(sql, probeTarget, projectRoot, projectConfig);
|
|
3516
|
+
const result = await executor.executeQuery(prepared.sql, [], runtimeVariables({}), prepared.connection);
|
|
3517
|
+
const rows = normalizeQueryResult(result).rows;
|
|
3518
|
+
if (rows.length === 0)
|
|
3519
|
+
return undefined;
|
|
3520
|
+
const tables = buildAgentSchemaContext('', rows, { includeUnscored: true, limit: 8 });
|
|
3521
|
+
if (tables.length === 0)
|
|
3522
|
+
return undefined;
|
|
3523
|
+
return {
|
|
3524
|
+
relations: tables.map((table) => ({
|
|
3525
|
+
relation: table.relation,
|
|
3526
|
+
name: table.name,
|
|
3527
|
+
// Provenance is explicit: these columns came from a live lookup, not
|
|
3528
|
+
// from the immutable snapshot.
|
|
3529
|
+
source: 'scoped live probe',
|
|
3530
|
+
columns: table.columns.map((column) => ({ name: column.name, type: column.type })),
|
|
3531
|
+
columnCompleteness: 'complete',
|
|
3532
|
+
})),
|
|
3533
|
+
schemaContext: tables.map((table) => ({
|
|
3534
|
+
relation: table.relation,
|
|
3535
|
+
schema: table.schema,
|
|
3536
|
+
name: table.name,
|
|
3537
|
+
columns: table.columns,
|
|
3538
|
+
})),
|
|
3539
|
+
notes: tables.map((table) => `${table.relation} resolved by a scoped live probe`),
|
|
3540
|
+
};
|
|
3541
|
+
};
|
|
3290
3542
|
const getSchemaContextForAgent = async (question, preparedContextPack, executionConnection, executionConnectionName) => {
|
|
3291
3543
|
const schemaConnection = executionConnection ?? connection;
|
|
3292
3544
|
const scanRuntimeSchema = async () => {
|
|
@@ -12135,6 +12387,60 @@ export async function startLocalServer(opts) {
|
|
|
12135
12387
|
res.end(readFileSync(filePath));
|
|
12136
12388
|
return;
|
|
12137
12389
|
}
|
|
12390
|
+
/**
|
|
12391
|
+
* Promote a RESULT COLUMN into a runtime filter input on a generated answer.
|
|
12392
|
+
*
|
|
12393
|
+
* The SQL edit happens here, not in the browser: deciding where a predicate
|
|
12394
|
+
* may go needs a real dialect parse, and shipping a SQL parser to the client
|
|
12395
|
+
* to guess at it is exactly how a filter ends up silently attached to the
|
|
12396
|
+
* wrong query. The client sends a column; it gets back an artifact.
|
|
12397
|
+
*/
|
|
12398
|
+
if (req.method === 'POST' && path === '/api/dql/artifacts/add-filter') {
|
|
12399
|
+
try {
|
|
12400
|
+
const body = await readJSON(req);
|
|
12401
|
+
const artifact = normalizeDqlArtifactReference(body.artifact);
|
|
12402
|
+
const column = typeof body.column === 'string' ? body.column.trim() : '';
|
|
12403
|
+
const value = body.value;
|
|
12404
|
+
const resultColumns = Array.isArray(body.resultColumns)
|
|
12405
|
+
? body.resultColumns.filter((entry) => typeof entry === 'string')
|
|
12406
|
+
: [];
|
|
12407
|
+
if (!artifact)
|
|
12408
|
+
throw new Error('A DQL artifact is required to add a filter input.');
|
|
12409
|
+
if (!column)
|
|
12410
|
+
throw new Error('Choose a result column to filter on.');
|
|
12411
|
+
if (value === undefined || value === null || String(value).trim() === '') {
|
|
12412
|
+
throw new Error('Choose a default filter value before adding the input.');
|
|
12413
|
+
}
|
|
12414
|
+
const sql = extractBlockStudioSql(artifact.source);
|
|
12415
|
+
if (!sql)
|
|
12416
|
+
throw new Error('This answer has no editable SQL to filter.');
|
|
12417
|
+
const activeConnection = requireActiveConnection();
|
|
12418
|
+
const existingNames = (artifact.parameters ?? []).map((parameter) => parameter.name);
|
|
12419
|
+
const added = addSqlResultFilter(sql, resultColumns, column, existingNames, activeConnection.driver);
|
|
12420
|
+
const source = replaceBlockStudioSql(artifact.source, added.sql, added.parameterName, value);
|
|
12421
|
+
// Re-parse so the returned contract is what the block ACTUALLY declares,
|
|
12422
|
+
// rather than what this endpoint believes it wrote.
|
|
12423
|
+
const invocation = prepareBlockInvocation({ source, surface: 'ask_ai' });
|
|
12424
|
+
if (invocation.errors.length > 0)
|
|
12425
|
+
throw new Error(invocation.errors.join(' '));
|
|
12426
|
+
const { executionReceipt: _receipt, compiledSql: _compiled, ...unexecuted } = artifact;
|
|
12427
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
12428
|
+
res.end(serializeJSON({
|
|
12429
|
+
artifact: {
|
|
12430
|
+
...unexecuted,
|
|
12431
|
+
source,
|
|
12432
|
+
parameters: invocation.parameters,
|
|
12433
|
+
parameterValues: { ...(artifact.parameterValues ?? {}), [added.parameterName]: value },
|
|
12434
|
+
},
|
|
12435
|
+
parameterName: added.parameterName,
|
|
12436
|
+
}));
|
|
12437
|
+
}
|
|
12438
|
+
catch (error) {
|
|
12439
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
12440
|
+
res.end(serializeJSON({ error: apiErrorMessage(error) }));
|
|
12441
|
+
}
|
|
12442
|
+
return;
|
|
12443
|
+
}
|
|
12138
12444
|
if (req.method === 'POST' && path === '/api/dql/artifacts/execute') {
|
|
12139
12445
|
try {
|
|
12140
12446
|
const body = await readJSON(req);
|
|
@@ -15093,18 +15399,80 @@ const AGENT_PREVIEW_FORBIDDEN_SQL = [
|
|
|
15093
15399
|
'update',
|
|
15094
15400
|
'vacuum',
|
|
15095
15401
|
];
|
|
15402
|
+
/**
|
|
15403
|
+
* Run DQL's own block-compiler machinery with an honest origin. Anything these
|
|
15404
|
+
* stages throw is a DQL-artifact failure, never a warehouse failure.
|
|
15405
|
+
*/
|
|
15406
|
+
function withAnalyticalCompilationOrigin(stage, fn) {
|
|
15407
|
+
return withAnalyticalErrorOriginSync({ origin: 'dql_compilation', stage }, fn);
|
|
15408
|
+
}
|
|
15409
|
+
export function clampAnalyticalRowBound(rowLimit) {
|
|
15410
|
+
return Number.isFinite(rowLimit) ? Math.max(1, Math.min(10_000, Math.floor(rowLimit))) : 200;
|
|
15411
|
+
}
|
|
15412
|
+
/**
|
|
15413
|
+
* Dialects where a bare trailing `LIMIT n` is valid on a top-level SELECT.
|
|
15414
|
+
* MSSQL/Fabric need `TOP`/`OFFSET…FETCH` in positions that depend on the rest
|
|
15415
|
+
* of the statement, so we never rewrite there — the host-side truncation below
|
|
15416
|
+
* is the guarantee, and appending is only an optimization.
|
|
15417
|
+
*/
|
|
15418
|
+
const TRAILING_LIMIT_DIALECTS = new Set([
|
|
15419
|
+
'duckdb', 'postgres', 'postgresql', 'redshift', 'snowflake', 'bigquery',
|
|
15420
|
+
'databricks', 'trino', 'presto', 'clickhouse', 'mysql', 'mariadb', 'sqlite', 'athena',
|
|
15421
|
+
]);
|
|
15422
|
+
/**
|
|
15423
|
+
* Bound a result set WITHOUT rewriting the statement's shape.
|
|
15424
|
+
*
|
|
15425
|
+
* The previous approach wrapped every Ask query as
|
|
15426
|
+
* `SELECT * FROM (<sql>) AS dql_agent_preview LIMIT n`. That is not a no-op:
|
|
15427
|
+
* - `SELECT * FROM (WITH x AS (…) SELECT …) AS t` is a syntax error on
|
|
15428
|
+
* MSSQL/Fabric, and the model emits CTEs constantly;
|
|
15429
|
+
* - duplicate output column names are legal at top level but not as a
|
|
15430
|
+
* derived table on several engines;
|
|
15431
|
+
* - the inner `ORDER BY` stops being guaranteed, silently breaking top-N.
|
|
15432
|
+
* All three produced "fails in Ask, runs in the notebook" for identical SQL.
|
|
15433
|
+
*
|
|
15434
|
+
* Appending is skipped whenever it cannot be proven safe. Callers must still
|
|
15435
|
+
* truncate rows host-side — that, not this, is what actually enforces the bound.
|
|
15436
|
+
*/
|
|
15437
|
+
export function buildRowBoundedSql(sql, rowLimit, dialect = 'duckdb') {
|
|
15438
|
+
const trimmed = sql.trim().replace(/;\s*$/, '').trim();
|
|
15439
|
+
if (!rowLimit)
|
|
15440
|
+
return { sql: trimmed, outcome: 'skipped', reason: 'no row bound requested' };
|
|
15441
|
+
if (!TRAILING_LIMIT_DIALECTS.has(dialect.toLowerCase())) {
|
|
15442
|
+
return { sql: trimmed, outcome: 'skipped', reason: `trailing LIMIT is not portable on ${dialect}` };
|
|
15443
|
+
}
|
|
15444
|
+
const analysis = analyzeSqlReferences(trimmed, dialect);
|
|
15445
|
+
if (!analysis.parsed) {
|
|
15446
|
+
return { sql: trimmed, outcome: 'skipped', reason: 'statement did not parse; not rewriting it' };
|
|
15447
|
+
}
|
|
15448
|
+
const statements = analysis.statementTypes ?? [];
|
|
15449
|
+
if (statements.length !== 1 || statements[0]?.toLowerCase() !== 'select') {
|
|
15450
|
+
return { sql: trimmed, outcome: 'skipped', reason: 'not a single SELECT statement' };
|
|
15451
|
+
}
|
|
15452
|
+
// Any existing bound — LIMIT, FETCH FIRST/NEXT, TOP — wins. Detection is
|
|
15453
|
+
// deliberately greedy: a false positive costs nothing (host truncation still
|
|
15454
|
+
// applies), a false negative would override the author's own bound.
|
|
15455
|
+
const scan = stripSqlStringsAndComments(trimmed);
|
|
15456
|
+
if (/\blimit\b/i.test(scan) || /\bfetch\s+(first|next)\b/i.test(scan) || /\btop\s*\(?\s*\d/i.test(scan)) {
|
|
15457
|
+
return { sql: trimmed, outcome: 'existing', reason: 'statement already carries its own bound' };
|
|
15458
|
+
}
|
|
15459
|
+
return { sql: `${trimmed}\nLIMIT ${clampAnalyticalRowBound(rowLimit)}`, outcome: 'appended' };
|
|
15460
|
+
}
|
|
15461
|
+
/**
|
|
15462
|
+
* @deprecated Kept for one release so any out-of-tree caller keeps working.
|
|
15463
|
+
* Use {@link executeAnalyticalSql}, which bounds rows without wrapping.
|
|
15464
|
+
*/
|
|
15096
15465
|
export function buildAgentPreviewSql(sql, rowLimit = 200) {
|
|
15097
15466
|
const trimmed = sql.trim();
|
|
15098
15467
|
if (!trimmed)
|
|
15099
|
-
throw
|
|
15100
|
-
const boundedRowLimit = Number.isFinite(rowLimit)
|
|
15101
|
-
? Math.max(1, Math.min(10_000, Math.floor(rowLimit)))
|
|
15102
|
-
: 200;
|
|
15468
|
+
throw analyticalError('Generated SQL preview is empty.', { origin: 'host', stage: 'execute' });
|
|
15103
15469
|
const withoutTrailingSemicolon = trimmed.replace(/;\s*$/, '').trim();
|
|
15104
15470
|
const readOnlyError = readOnlySqlValidationError(withoutTrailingSemicolon, 'Generated SQL preview');
|
|
15105
|
-
|
|
15106
|
-
|
|
15107
|
-
|
|
15471
|
+
// A deliberate read-only refusal, not a warehouse rejection.
|
|
15472
|
+
if (readOnlyError) {
|
|
15473
|
+
throw analyticalError(readOnlyError, { origin: 'governance_gate', stage: 'validation', code: 'unsafe_sql' });
|
|
15474
|
+
}
|
|
15475
|
+
return buildRowBoundedSql(withoutTrailingSemicolon, rowLimit).sql;
|
|
15108
15476
|
}
|
|
15109
15477
|
/**
|
|
15110
15478
|
* EXP-001 preflight for model-authored exploratory SQL.
|
|
@@ -15492,7 +15860,7 @@ function summarizeExploratoryJoinProbe(rows, leftRelation, leftColumn, rightRela
|
|
|
15492
15860
|
};
|
|
15493
15861
|
return `${leftRelation}.${leftColumn} ↔ ${rightRelation}.${rightColumn}: sample left=${number('left_sample_rows')}, right=${number('right_sample_rows')}, joined=${number('joined_rows')}, unmatched left/right=${number('unmatched_left_sample_rows')}/${number('unmatched_right_sample_rows')}, max matches left/right=${number('max_matches_per_left_key')}/${number('max_matches_per_right_key')}`;
|
|
15494
15862
|
}
|
|
15495
|
-
function readOnlySqlValidationError(sql, subject) {
|
|
15863
|
+
function readOnlySqlValidationError(sql, subject, dialect = 'duckdb') {
|
|
15496
15864
|
const internalRelationError = internalDqlRelationIdValidationError(sql, subject);
|
|
15497
15865
|
if (internalRelationError)
|
|
15498
15866
|
return internalRelationError;
|
|
@@ -15503,6 +15871,22 @@ function readOnlySqlValidationError(sql, subject) {
|
|
|
15503
15871
|
if (scanSql.includes(';')) {
|
|
15504
15872
|
return `${subject} only supports one statement.`;
|
|
15505
15873
|
}
|
|
15874
|
+
// Prefer the parser. The keyword blacklist matches IDENTIFIERS, so a CTE or
|
|
15875
|
+
// column alias legitimately named `set`, `load`, `merge`, or `copy` was
|
|
15876
|
+
// rejected as if it were DDL — a false failure on SQL the notebook runs fine.
|
|
15877
|
+
// The blacklist stays as the conservative fallback for statements the dialect
|
|
15878
|
+
// parser cannot read.
|
|
15879
|
+
const analysis = analyzeSqlReferences(scanSql, dialect);
|
|
15880
|
+
if (analysis.parsed) {
|
|
15881
|
+
const statements = analysis.statementTypes ?? [];
|
|
15882
|
+
const offending = statements.find((statement) => statement.toLowerCase() !== 'select');
|
|
15883
|
+
if (offending) {
|
|
15884
|
+
return `${subject} rejected unsupported statement keyword: ${offending.toUpperCase()}.`;
|
|
15885
|
+
}
|
|
15886
|
+
if (statements.length > 1)
|
|
15887
|
+
return `${subject} only supports one statement.`;
|
|
15888
|
+
return null;
|
|
15889
|
+
}
|
|
15506
15890
|
const forbiddenPattern = new RegExp(`\\b(${AGENT_PREVIEW_FORBIDDEN_SQL.join('|')})\\b`, 'i');
|
|
15507
15891
|
const forbidden = scanSql.match(forbiddenPattern)?.[1];
|
|
15508
15892
|
if (forbidden) {
|
|
@@ -18804,7 +19188,7 @@ function agentResultToSynthesisPreview(result) {
|
|
|
18804
19188
|
}
|
|
18805
19189
|
/** Up to three example questions built from real catalog blocks (falls back to generic asks). */
|
|
18806
19190
|
function buildConversationSuggestions(projectRoot, kind) {
|
|
18807
|
-
if (kind === 'gratitude')
|
|
19191
|
+
if (kind === 'gratitude' || kind === 'answer_explanation')
|
|
18808
19192
|
return [];
|
|
18809
19193
|
let names = [];
|
|
18810
19194
|
try {
|
|
@@ -18900,9 +19284,9 @@ export function buildConversationContextRecap(context) {
|
|
|
18900
19284
|
return undefined;
|
|
18901
19285
|
return `We were talking about ${contextBits.join('. ')}.`;
|
|
18902
19286
|
}
|
|
18903
|
-
function
|
|
19287
|
+
function latestSubstantiveConversationTurn(context) {
|
|
18904
19288
|
const turns = Array.isArray(context.turns)
|
|
18905
|
-
? context.turns.map(agentRunRecord).filter((turn) => Boolean(turn)).slice(-
|
|
19289
|
+
? context.turns.map(agentRunRecord).filter((turn) => Boolean(turn)).slice(-6)
|
|
18906
19290
|
: [];
|
|
18907
19291
|
if (turns.length === 0)
|
|
18908
19292
|
return undefined;
|
|
@@ -18914,9 +19298,120 @@ function buildConversationTurnsRecap(context) {
|
|
|
18914
19298
|
// subject of the next recap. Prefer the newest analytical/authoring turn and
|
|
18915
19299
|
// fall back to the active turn only when the thread contains conversation alone.
|
|
18916
19300
|
const substantiveTurns = turns.filter((turn) => agentRunString(turn.route) !== 'conversation');
|
|
18917
|
-
|
|
19301
|
+
return active && agentRunString(active.route) !== 'conversation'
|
|
18918
19302
|
? active
|
|
18919
19303
|
: substantiveTurns[substantiveTurns.length - 1] ?? active ?? turns[turns.length - 1];
|
|
19304
|
+
}
|
|
19305
|
+
/**
|
|
19306
|
+
* Explain the latest successful answer from its persisted artifact contract.
|
|
19307
|
+
* This is intentionally deterministic and never executes SQL: missing evidence
|
|
19308
|
+
* is reported as missing instead of being guessed or converted into a new metric.
|
|
19309
|
+
*/
|
|
19310
|
+
export function buildPriorAnswerExplanation(question, context) {
|
|
19311
|
+
if (!context)
|
|
19312
|
+
return undefined;
|
|
19313
|
+
const selected = latestSubstantiveConversationTurn(context);
|
|
19314
|
+
const artifact = agentRunRecord(selected?.dqlArtifact) ?? agentRunRecord(context.dqlArtifact);
|
|
19315
|
+
const result = agentRunRecord(selected?.result);
|
|
19316
|
+
const metrics = agentRunStringArray(artifact?.metrics
|
|
19317
|
+
?? selected?.requestedMeasures
|
|
19318
|
+
?? context.priorMeasures);
|
|
19319
|
+
const dimensions = agentRunStringArray(artifact?.dimensions
|
|
19320
|
+
?? selected?.requestedDimensions
|
|
19321
|
+
?? context.requestedDimensions);
|
|
19322
|
+
const timeDimension = agentRunRecord(artifact?.timeDimension);
|
|
19323
|
+
const timeName = agentRunString(timeDimension?.name);
|
|
19324
|
+
const granularity = agentRunString(timeDimension?.granularity);
|
|
19325
|
+
const filters = formatPriorAnswerFilters(artifact?.filters, selected?.requestedFilters ?? context.requestedFilters);
|
|
19326
|
+
const topN = typeof selected?.topN === 'number'
|
|
19327
|
+
? selected.topN
|
|
19328
|
+
: typeof context.priorLimit === 'number'
|
|
19329
|
+
? context.priorLimit
|
|
19330
|
+
: undefined;
|
|
19331
|
+
const source = agentRunString(artifact?.name)
|
|
19332
|
+
?? agentRunString(selected?.sourceCertifiedBlock)
|
|
19333
|
+
?? agentRunString(context.sourceCertifiedBlock);
|
|
19334
|
+
const columns = agentRunStringArray(result?.columns ?? context.resultColumns).slice(0, 8);
|
|
19335
|
+
const metricText = metrics.length > 0 ? metrics.map(humanizeArtifactField).join(', ') : undefined;
|
|
19336
|
+
const dimensionText = dimensions.length > 0 ? dimensions.map(humanizeArtifactField).join(', ') : undefined;
|
|
19337
|
+
const normalizedQuestion = question.toLowerCase();
|
|
19338
|
+
if (/\b(?:daily|monthly|weekly|quarterly|yearly|annual|grain|period|timeframe|date range)\b/.test(normalizedQuestion)) {
|
|
19339
|
+
if (granularity) {
|
|
19340
|
+
return [
|
|
19341
|
+
`The previous result uses${metricText ? ` ${metricText}` : ' the recorded metric'} and is grouped`
|
|
19342
|
+
+ `${timeName ? ` by ${humanizeArtifactField(timeName)}` : ''} at a ${formatTimeGranularity(granularity)} grain.`,
|
|
19343
|
+
filters.length > 0 ? `Applied filters: ${filters.join('; ')}.` : '',
|
|
19344
|
+
].filter(Boolean).join(' ');
|
|
19345
|
+
}
|
|
19346
|
+
return [
|
|
19347
|
+
`The previous result does not declare a daily or monthly time grain${metricText ? ` for ${metricText}` : ''}.`,
|
|
19348
|
+
dimensionText ? `Its recorded grouping is ${dimensionText}.` : 'It is an aggregate across the applied result scope.',
|
|
19349
|
+
filters.length > 0 ? `Applied filters: ${filters.join('; ')}.` : '',
|
|
19350
|
+
'If you want a new monthly or daily breakdown, ask me to show or group it that way.',
|
|
19351
|
+
].filter(Boolean).join(' ');
|
|
19352
|
+
}
|
|
19353
|
+
if (/\b(?:which|what)\s+(?:metric|measure)\b|\bmetric\s+(?:did|does|was|is)\b/.test(normalizedQuestion)) {
|
|
19354
|
+
return metricText
|
|
19355
|
+
? `The previous answer used ${metricText}${source ? ` from ${humanizeArtifactField(source)}` : ''}.`
|
|
19356
|
+
: 'The previous answer did not retain a qualified metric identifier, so I cannot name one without rerunning or reviewing its DQL.';
|
|
19357
|
+
}
|
|
19358
|
+
if (/\bfilter|include|exclude\b/.test(normalizedQuestion)) {
|
|
19359
|
+
return filters.length > 0
|
|
19360
|
+
? `The previous answer applied these filters: ${filters.join('; ')}.`
|
|
19361
|
+
: 'The previous artifact does not record any explicit filters.';
|
|
19362
|
+
}
|
|
19363
|
+
const facts = [
|
|
19364
|
+
metricText ? `Metric: ${metricText}.` : '',
|
|
19365
|
+
dimensionText ? `Grouped by: ${dimensionText}.` : '',
|
|
19366
|
+
granularity ? `Time grain: ${formatTimeGranularity(granularity)}${timeName ? ` on ${humanizeArtifactField(timeName)}` : ''}.` : '',
|
|
19367
|
+
filters.length > 0 ? `Filters: ${filters.join('; ')}.` : '',
|
|
19368
|
+
topN ? `Limit: top ${topN}.` : '',
|
|
19369
|
+
source ? `Source: ${humanizeArtifactField(source)}.` : '',
|
|
19370
|
+
columns.length > 0 ? `Returned fields: ${columns.map(humanizeArtifactField).join(', ')}.` : '',
|
|
19371
|
+
].filter(Boolean);
|
|
19372
|
+
return facts.length > 0
|
|
19373
|
+
? facts.join(' ')
|
|
19374
|
+
: 'The previous answer did not retain enough artifact metadata to explain its metric, filters, or grain safely.';
|
|
19375
|
+
}
|
|
19376
|
+
function formatPriorAnswerFilters(primary, fallback) {
|
|
19377
|
+
const fromArtifact = Array.isArray(primary)
|
|
19378
|
+
? primary.map(agentRunRecord).filter((filter) => Boolean(filter)).flatMap((filter) => {
|
|
19379
|
+
const dimension = agentRunString(filter.dimension);
|
|
19380
|
+
const operator = agentRunString(filter.operator) ?? 'equals';
|
|
19381
|
+
const values = agentRunStringArray(filter.values);
|
|
19382
|
+
return dimension && values.length > 0
|
|
19383
|
+
? [`${humanizeArtifactField(dimension)} ${operator.replace(/_/g, ' ')} ${values.join(', ')}`]
|
|
19384
|
+
: [];
|
|
19385
|
+
})
|
|
19386
|
+
: [];
|
|
19387
|
+
return fromArtifact.length > 0
|
|
19388
|
+
? fromArtifact
|
|
19389
|
+
: agentRunStringArray(fallback).map((value) => value.replace(/_/g, ' '));
|
|
19390
|
+
}
|
|
19391
|
+
function humanizeArtifactField(value) {
|
|
19392
|
+
return value
|
|
19393
|
+
.split('.')
|
|
19394
|
+
.at(-1)
|
|
19395
|
+
.replace(/::/g, ' ')
|
|
19396
|
+
.replace(/[_-]+/g, ' ')
|
|
19397
|
+
.replace(/\s+/g, ' ')
|
|
19398
|
+
.trim();
|
|
19399
|
+
}
|
|
19400
|
+
function formatTimeGranularity(value) {
|
|
19401
|
+
const normalized = value.trim().toLowerCase();
|
|
19402
|
+
return {
|
|
19403
|
+
day: 'daily',
|
|
19404
|
+
week: 'weekly',
|
|
19405
|
+
month: 'monthly',
|
|
19406
|
+
quarter: 'quarterly',
|
|
19407
|
+
year: 'yearly',
|
|
19408
|
+
}[normalized] ?? humanizeArtifactField(value);
|
|
19409
|
+
}
|
|
19410
|
+
function buildConversationTurnsRecap(context) {
|
|
19411
|
+
const turns = Array.isArray(context.turns)
|
|
19412
|
+
? context.turns.map(agentRunRecord).filter((turn) => Boolean(turn)).slice(-3)
|
|
19413
|
+
: [];
|
|
19414
|
+
const selected = latestSubstantiveConversationTurn(context);
|
|
18920
19415
|
if (!selected)
|
|
18921
19416
|
return undefined;
|
|
18922
19417
|
const result = agentRunRecord(selected.result);
|
|
@@ -22423,6 +22918,51 @@ const RUNTIME_SCHEMA_SEARCH_STOPWORDS = new Set([
|
|
|
22423
22918
|
* execution target. Tokens are reduced to safe identifier characters before
|
|
22424
22919
|
* interpolation, so user text cannot become SQL syntax.
|
|
22425
22920
|
*/
|
|
22921
|
+
/**
|
|
22922
|
+
* Point-lookup of NAMED relations in `information_schema.columns`.
|
|
22923
|
+
*
|
|
22924
|
+
* This is deliberately not `buildRuntimeSchemaSearchSql`. That one matches
|
|
22925
|
+
* `LOWER(table_name) LIKE '%term%'` across every visible schema, which is the
|
|
22926
|
+
* unbounded rescan the "a lexical miss is not permission to rescan" policy
|
|
22927
|
+
* exists to prevent. This one asks about specific `(schema, table)` pairs the
|
|
22928
|
+
* model actually referenced, with equality predicates — a metadata point
|
|
22929
|
+
* lookup, not a scan.
|
|
22930
|
+
*
|
|
22931
|
+
* Returns null when nothing safe to ask about survives normalization.
|
|
22932
|
+
*/
|
|
22933
|
+
export function buildNamedRelationProbeSql(relations, maxRelations = 4) {
|
|
22934
|
+
const predicates = [];
|
|
22935
|
+
const seen = new Set();
|
|
22936
|
+
for (const raw of relations) {
|
|
22937
|
+
const parts = raw.replace(/["`\[\]]/g, '').trim().split('.').filter(Boolean);
|
|
22938
|
+
const table = parts.at(-1);
|
|
22939
|
+
// Identifiers only — never interpolate anything that could carry SQL.
|
|
22940
|
+
if (!table || !/^[A-Za-z_][A-Za-z0-9_$]*$/.test(table))
|
|
22941
|
+
continue;
|
|
22942
|
+
const schema = parts.length >= 2 ? parts.at(-2) : undefined;
|
|
22943
|
+
if (schema && !/^[A-Za-z_][A-Za-z0-9_$]*$/.test(schema))
|
|
22944
|
+
continue;
|
|
22945
|
+
const key = `${schema ?? ''}.${table}`.toLowerCase();
|
|
22946
|
+
if (seen.has(key))
|
|
22947
|
+
continue;
|
|
22948
|
+
seen.add(key);
|
|
22949
|
+
predicates.push(schema
|
|
22950
|
+
? `(LOWER(table_schema) = '${schema.toLowerCase()}' AND LOWER(table_name) = '${table.toLowerCase()}')`
|
|
22951
|
+
: `(LOWER(table_name) = '${table.toLowerCase()}')`);
|
|
22952
|
+
if (predicates.length >= maxRelations)
|
|
22953
|
+
break;
|
|
22954
|
+
}
|
|
22955
|
+
if (predicates.length === 0)
|
|
22956
|
+
return null;
|
|
22957
|
+
return `SELECT table_schema, table_name, column_name, data_type
|
|
22958
|
+
FROM information_schema.columns
|
|
22959
|
+
WHERE UPPER(table_schema) NOT IN ('INFORMATION_SCHEMA', 'PG_CATALOG')
|
|
22960
|
+
AND (
|
|
22961
|
+
${predicates.map((predicate) => ` ${predicate}`).join(' OR\n')}
|
|
22962
|
+
)
|
|
22963
|
+
ORDER BY table_schema, table_name, ordinal_position
|
|
22964
|
+
LIMIT 400`;
|
|
22965
|
+
}
|
|
22426
22966
|
export function buildRuntimeSchemaSearchSql(question) {
|
|
22427
22967
|
const terms = Array.from(agentSchemaTokens(question))
|
|
22428
22968
|
.map((term) => term.toLowerCase().replace(/[^a-z0-9_]/g, ''))
|