@finos/legend-lego 2.0.217 → 2.0.219
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/lib/index.css +2 -2
- package/lib/index.css.map +1 -1
- package/lib/legend-ai/LegendAIDocEnrichment.d.ts +14 -0
- package/lib/legend-ai/LegendAIDocEnrichment.d.ts.map +1 -1
- package/lib/legend-ai/LegendAIDocEnrichment.js +140 -17
- package/lib/legend-ai/LegendAIDocEnrichment.js.map +1 -1
- package/lib/legend-ai/LegendAITypes.d.ts +30 -1
- package/lib/legend-ai/LegendAITypes.d.ts.map +1 -1
- package/lib/legend-ai/LegendAITypes.js +31 -0
- package/lib/legend-ai/LegendAITypes.js.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts +2 -2
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js.map +1 -1
- package/lib/legend-ai/components/LegendAIChat.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIChat.js +62 -38
- package/lib/legend-ai/components/LegendAIChat.js.map +1 -1
- package/lib/legend-ai/components/LegendAIChatHelpers.d.ts +5 -0
- package/lib/legend-ai/components/LegendAIChatHelpers.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIChatHelpers.js +8 -1
- package/lib/legend-ai/components/LegendAIChatHelpers.js.map +1 -1
- package/lib/legend-ai/index.d.ts +3 -2
- package/lib/legend-ai/index.d.ts.map +1 -1
- package/lib/legend-ai/index.js +3 -2
- package/lib/legend-ai/index.js.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts +27 -2
- package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatProcessors.js +329 -86
- package/lib/legend-ai/stores/LegendAIChatProcessors.js.map +1 -1
- package/lib/legend-ai/stores/LegendAIJoinAnalysis.d.ts +11 -0
- package/lib/legend-ai/stores/LegendAIJoinAnalysis.d.ts.map +1 -1
- package/lib/legend-ai/stores/LegendAIJoinAnalysis.js +53 -19
- package/lib/legend-ai/stores/LegendAIJoinAnalysis.js.map +1 -1
- package/lib/legend-ai/stores/LegendAISqlHelpers.d.ts +4 -1
- package/lib/legend-ai/stores/LegendAISqlHelpers.d.ts.map +1 -1
- package/lib/legend-ai/stores/LegendAISqlHelpers.js +25 -2
- package/lib/legend-ai/stores/LegendAISqlHelpers.js.map +1 -1
- package/lib/legend-ai/stores/LegendAISqlJoinSanitizers.js +4 -4
- package/lib/legend-ai/stores/LegendAISqlJoinSanitizers.js.map +1 -1
- package/package.json +3 -3
- package/src/legend-ai/LegendAIDocEnrichment.ts +189 -23
- package/src/legend-ai/LegendAITypes.ts +54 -1
- package/src/legend-ai/LegendAI_LegendApplicationPlugin_Extension.ts +2 -0
- package/src/legend-ai/components/LegendAIChat.tsx +114 -78
- package/src/legend-ai/components/LegendAIChatHelpers.ts +9 -3
- package/src/legend-ai/index.ts +7 -0
- package/src/legend-ai/stores/LegendAIChatProcessors.ts +476 -105
- package/src/legend-ai/stores/LegendAIJoinAnalysis.ts +82 -19
- package/src/legend-ai/stores/LegendAISqlHelpers.ts +32 -2
- package/src/legend-ai/stores/LegendAISqlJoinSanitizers.ts +4 -4
|
@@ -17,11 +17,11 @@ import { assertErrorThrown, guaranteeNonNullable, isNonNullable, uuid, } from '@
|
|
|
17
17
|
import { LegendAISelfHealKind, LegendAIQuestionIntent, LegendAIResponseOutcome, LegendAIThinkingStepStatus, LegendAIMessageRole, LegendAIErrorType, LegendAIExecutionTimeoutError, LegendAIServiceError, LegendAIUnsupportedEngineShapeError, TDSServiceSourceType, buildColumnDefsFromNames, LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID, LEGEND_AI_ALTERNATE_ROOT_ACTION_ID, LEGEND_AI_FEEDBACK_PROMPT, getTodayISO, } from '../LegendAITypes.js';
|
|
18
18
|
import { LegendAIJudgeVerdict, } from '../LegendAI_LegendApplicationPlugin_Extension.js';
|
|
19
19
|
import { extractElementNameFromPath, } from '@finos/legend-graph';
|
|
20
|
-
import { buildEnrichedBusinessContext, buildModelContextEnrichmentText, buildModelCatalogText, buildDataQueryApproachText, findBestAlternateRoot, rankEntities, relaxExactStringFilters, extractFilteredColumns, buildProbedValueHints, splitIdentifierTokens, tokenizeText, } from '../LegendAIDocEnrichment.js';
|
|
20
|
+
import { buildEnrichedBusinessContext, buildModelContextEnrichmentText, buildModelCatalogText, buildDataQueryApproachText, findBestAlternateRoot, rankEntities, relaxExactStringFilters, extractFilteredColumns, buildProbedValueHints, buildPriorSqlFailureHints, splitIdentifierTokens, tokenizeText, } from '../LegendAIDocEnrichment.js';
|
|
21
21
|
import { isFuzzyMatch, preFilterServicesByRelevance, } from '../LegendAIServiceRetrieval.js';
|
|
22
|
-
import { isNumericColumn, isStringColumn, } from '../components/LegendAIChatHelpers.js';
|
|
23
|
-
import { HAS_LIMIT_PATTERN,
|
|
24
|
-
import { buildCrossJoinZeroRowExplanation, buildJoinablePairSuggestions, } from './LegendAIJoinAnalysis.js';
|
|
22
|
+
import { isNumericColumn, isStringColumn, isStringTypedColumn, } from '../components/LegendAIChatHelpers.js';
|
|
23
|
+
import { buildColumnByNameIndex, accessPointName, HAS_LIMIT_PATTERN, accessPointCalls, servicePId, pureRelationColumnRef, extractJoinKeyColumns, sharedColumnNames, } from './LegendAISqlHelpers.js';
|
|
24
|
+
import { buildCrossJoinZeroRowExplanation, buildJoinablePairSuggestions, detectDisjointJoinUniverses, resolveJoinedAccessPoints, buildDisjointJoinMessage, previewValues, } from './LegendAIJoinAnalysis.js';
|
|
25
25
|
import { boundCrossAccessPointJoinDrivingSide, sanitizeJoinDuplicateColumns, sanitizeJoinOrderBy, sanitizeJoinSameKeyColumns, wrapBareJoinAccessPoints, } from './LegendAISqlJoinSanitizers.js';
|
|
26
26
|
const MAX_ERROR_MESSAGE_LENGTH = 500;
|
|
27
27
|
const MAX_DISPLAYED_ERROR_LENGTH = 4000;
|
|
@@ -32,6 +32,10 @@ const ANALYSIS_TIMEOUT_MS = 15_000;
|
|
|
32
32
|
const ORCHESTRATOR_GENERATION_TIMEOUT_MS = 120_000;
|
|
33
33
|
const DISTINCT_PROBE_ROW_LIMIT = 5000;
|
|
34
34
|
const MAX_RECOVERED_FILTER_COLUMNS = 4;
|
|
35
|
+
export const JOIN_OVERLAP_PROBE_LIMIT = 200;
|
|
36
|
+
const JOIN_OVERLAP_CONCLUSIVE_MAX = JOIN_OVERLAP_PROBE_LIMIT - 1;
|
|
37
|
+
const MAX_JOIN_OVERLAP_PROBE_KEYS = 4;
|
|
38
|
+
const JOIN_OVERLAP_PROBE_TIMEOUT_MS = 15_000;
|
|
35
39
|
const EXECUTION_TIMEOUT_MS = 300_000;
|
|
36
40
|
const ANALYSIS_PREVIEW_ROW_LIMIT = 3;
|
|
37
41
|
const ANALYSIS_PREVIEW_VALUE_LIMIT = 40;
|
|
@@ -42,11 +46,28 @@ const LITERAL_COL_PATTERN = /,\s*'[^']*'\s+AS\s+(?:"[^"]+"|[a-z]\w*)/gi;
|
|
|
42
46
|
const SERVICE_CALL_PATTERN = /\bservice\s*\([^()]*\)/gi;
|
|
43
47
|
const DEFAULT_SAFETY_LIMIT = 1000;
|
|
44
48
|
const HAS_AGGREGATION_PATTERN = /\bGROUP\s+BY\b|\b(?:COUNT|SUM|AVG|MIN|MAX)\s*\(|\bSELECT\s+DISTINCT\b|\bHAVING\b|\bQUALIFY\b/i;
|
|
49
|
+
const OUTER_JOIN_PATTERN = /\b(?:LEFT|RIGHT|FULL)\s+(?:OUTER\s+)?JOIN\b/i;
|
|
50
|
+
const DISJUNCTIVE_PREDICATE_PATTERN = /\bOR\b/i;
|
|
51
|
+
const SET_OPERATION_PATTERN = /\b(?:UNION|INTERSECT|EXCEPT)\b/i;
|
|
45
52
|
const MAX_SERVICES_FOR_LLM_SELECTION = 30;
|
|
46
53
|
const VALUE_GROUNDING_TIME_BUDGET_MS = 4000;
|
|
47
54
|
const AP_SQL_EXECUTION_TIMEOUT_MS = 90_000;
|
|
48
55
|
const AP_SQL_EXECUTION_TIMEOUT_MESSAGE = 'This query is scanning very large access-point feeds and is taking too long to return. Narrow it with a date or entity filter, join on the full key (including the date), or use a *_LATEST snapshot access point.';
|
|
49
56
|
const ORCHESTRATOR_FALLBACK_LABEL = 'Try Legend AI Orchestrator';
|
|
57
|
+
// The single orchestrator fallback action, so every dead-end path offers the
|
|
58
|
+
// same button and threads any prior SQL failure the same way.
|
|
59
|
+
export function buildOrchestratorFallbackAction(failure) {
|
|
60
|
+
return {
|
|
61
|
+
label: ORCHESTRATOR_FALLBACK_LABEL,
|
|
62
|
+
actionId: LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
|
|
63
|
+
...(failure?.failedSql === undefined
|
|
64
|
+
? {}
|
|
65
|
+
: { failedSql: failure.failedSql }),
|
|
66
|
+
...(failure?.failedReason === undefined
|
|
67
|
+
? {}
|
|
68
|
+
: { failedReason: failure.failedReason }),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
50
71
|
const SQL_GENERATION_FAILURE_WITH_ORCHESTRATOR = 'SQL generation could not handle this query. You can try the Legend AI Orchestrator to generate a Pure query instead.';
|
|
51
72
|
const SQL_GENERATION_FAILURE_NO_ORCHESTRATOR = 'SQL generation could not handle this query. Try rephrasing your question.';
|
|
52
73
|
const SERVICE_PARAM_DATE_LIKE_PATTERNS = [
|
|
@@ -91,29 +112,32 @@ function deduplicateColumns(columns) {
|
|
|
91
112
|
return count === 0 ? col : `${col}_${count + 1}`;
|
|
92
113
|
});
|
|
93
114
|
}
|
|
115
|
+
export function createAssistantMessage() {
|
|
116
|
+
return {
|
|
117
|
+
id: uuid(),
|
|
118
|
+
role: LegendAIMessageRole.ASSISTANT,
|
|
119
|
+
thinkingSteps: [],
|
|
120
|
+
sql: null,
|
|
121
|
+
textAnswer: null,
|
|
122
|
+
dataContext: null,
|
|
123
|
+
gridData: null,
|
|
124
|
+
error: null,
|
|
125
|
+
errorType: null,
|
|
126
|
+
sqlGenTime: null,
|
|
127
|
+
execTime: null,
|
|
128
|
+
thinkingDuration: null,
|
|
129
|
+
isProcessing: true,
|
|
130
|
+
isExecuting: false,
|
|
131
|
+
suggestedQueries: [],
|
|
132
|
+
fallbackAction: null,
|
|
133
|
+
queriedAccessPointGroups: [],
|
|
134
|
+
queriedAccessPoints: [],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
94
137
|
export function createMessagePair(text) {
|
|
95
138
|
return [
|
|
96
139
|
{ id: uuid(), role: LegendAIMessageRole.USER, text },
|
|
97
|
-
|
|
98
|
-
id: uuid(),
|
|
99
|
-
role: LegendAIMessageRole.ASSISTANT,
|
|
100
|
-
thinkingSteps: [],
|
|
101
|
-
sql: null,
|
|
102
|
-
textAnswer: null,
|
|
103
|
-
dataContext: null,
|
|
104
|
-
gridData: null,
|
|
105
|
-
error: null,
|
|
106
|
-
errorType: null,
|
|
107
|
-
sqlGenTime: null,
|
|
108
|
-
execTime: null,
|
|
109
|
-
thinkingDuration: null,
|
|
110
|
-
isProcessing: true,
|
|
111
|
-
isExecuting: false,
|
|
112
|
-
suggestedQueries: [],
|
|
113
|
-
fallbackAction: null,
|
|
114
|
-
queriedAccessPointGroups: [],
|
|
115
|
-
queriedAccessPoints: [],
|
|
116
|
-
},
|
|
140
|
+
createAssistantMessage(),
|
|
117
141
|
];
|
|
118
142
|
}
|
|
119
143
|
export function updateLastAssistant(setMessages, updater) {
|
|
@@ -236,7 +260,7 @@ export function filterHistoryForAccessPoints(history, currentAccessPoints) {
|
|
|
236
260
|
function orderedAccessPointsFromSql(sql) {
|
|
237
261
|
const ordered = [];
|
|
238
262
|
const seen = new Set();
|
|
239
|
-
for (const match of sql
|
|
263
|
+
for (const match of accessPointCalls(sql)) {
|
|
240
264
|
const path = match.groups?.pId ?? '';
|
|
241
265
|
const dotIdx = path.lastIndexOf('.');
|
|
242
266
|
const apName = dotIdx === -1 ? path : path.slice(dotIdx + 1);
|
|
@@ -247,6 +271,8 @@ function orderedAccessPointsFromSql(sql) {
|
|
|
247
271
|
}
|
|
248
272
|
return ordered;
|
|
249
273
|
}
|
|
274
|
+
const MAX_HISTORY_ANSWER_LENGTH = 1500;
|
|
275
|
+
const MAX_CONVERSATION_HISTORY_TURNS = 10;
|
|
250
276
|
function buildTurnFromAssistant(userText, asstMsg) {
|
|
251
277
|
if (asstMsg.sql) {
|
|
252
278
|
const turn = {
|
|
@@ -274,7 +300,7 @@ function buildTurnFromAssistant(userText, asstMsg) {
|
|
|
274
300
|
if (asstMsg.textAnswer) {
|
|
275
301
|
return {
|
|
276
302
|
question: userText,
|
|
277
|
-
sql: asstMsg.textAnswer,
|
|
303
|
+
sql: asstMsg.textAnswer.slice(0, MAX_HISTORY_ANSWER_LENGTH),
|
|
278
304
|
intent: LegendAIQuestionIntent.METADATA,
|
|
279
305
|
};
|
|
280
306
|
}
|
|
@@ -309,6 +335,10 @@ export function classifyResponseOutcome(message) {
|
|
|
309
335
|
}
|
|
310
336
|
return LegendAIResponseOutcome.NO_ANSWER;
|
|
311
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* Pairs each question with the assistant turn that answered it, keeping only
|
|
340
|
+
* the most recent turns so prompts do not grow with conversation length.
|
|
341
|
+
*/
|
|
312
342
|
export function buildConversationHistory(messages) {
|
|
313
343
|
const history = [];
|
|
314
344
|
let i = 0;
|
|
@@ -327,7 +357,7 @@ export function buildConversationHistory(messages) {
|
|
|
327
357
|
i += 1;
|
|
328
358
|
}
|
|
329
359
|
}
|
|
330
|
-
return history;
|
|
360
|
+
return history.slice(-MAX_CONVERSATION_HISTORY_TURNS);
|
|
331
361
|
}
|
|
332
362
|
function formatServiceParams(services) {
|
|
333
363
|
return services.flatMap((s) => {
|
|
@@ -690,18 +720,103 @@ export function attachMetadataOverview(setMessages, metadataOverview) {
|
|
|
690
720
|
};
|
|
691
721
|
});
|
|
692
722
|
}
|
|
723
|
+
const PRODUCT_OVERVIEW_PATTERNS = [
|
|
724
|
+
/\bwhat\s+does\b.*\b(?:offer|provide|contain|have|include|cover)\b/,
|
|
725
|
+
/\b(?:describe|summarize|overview|summary)\b/,
|
|
726
|
+
/\bwhat\s+(?:access\s*points?|services?|data|datasets?)\b/,
|
|
727
|
+
/\bwhat\s+can\s+(?:i|we|you)\b/,
|
|
728
|
+
/\btell\s+me\s+(?:about|more)\b/,
|
|
729
|
+
];
|
|
730
|
+
const LISTING_VERB_PATTERN = /\b(?:list|show|what\s+are)\b/;
|
|
731
|
+
const LISTING_NOUN_PATTERN = /\b(?:access\s*points?|services?|endpoints?|datasets?|tables?)\b/;
|
|
732
|
+
export function isProductOverviewQuestion(question) {
|
|
733
|
+
const q = question.toLowerCase();
|
|
734
|
+
return (PRODUCT_OVERVIEW_PATTERNS.some((pattern) => pattern.test(q)) ||
|
|
735
|
+
(LISTING_VERB_PATTERN.test(q) && LISTING_NOUN_PATTERN.test(q)));
|
|
736
|
+
}
|
|
737
|
+
const CATALOG_MIN_SERVICES = 10;
|
|
738
|
+
const CATALOG_MAX_ENTRIES = 200;
|
|
739
|
+
// Buckets services under their access point group, groups in title order.
|
|
740
|
+
function groupServicesByAccessPointGroup(services) {
|
|
741
|
+
const byGroup = new Map();
|
|
742
|
+
for (const svc of services) {
|
|
743
|
+
const group = svc.accessPointGroupTitle ?? '';
|
|
744
|
+
const existing = byGroup.get(group);
|
|
745
|
+
if (existing) {
|
|
746
|
+
existing.push(svc);
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
byGroup.set(group, [svc]);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return Array.from(byGroup.entries()).sort((a, b) => a[0].localeCompare(b[0]));
|
|
753
|
+
}
|
|
754
|
+
// Renders each group as a heading followed by its services, up to the cap.
|
|
755
|
+
function renderCatalogEntries(groups) {
|
|
756
|
+
const lines = [];
|
|
757
|
+
let rendered = 0;
|
|
758
|
+
for (const [group, entries] of groups) {
|
|
759
|
+
if (rendered >= CATALOG_MAX_ENTRIES) {
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
if (group) {
|
|
763
|
+
lines.push(`\n### ${group}`);
|
|
764
|
+
}
|
|
765
|
+
for (const svc of [...entries].sort((a, b) => a.title.localeCompare(b.title))) {
|
|
766
|
+
if (rendered >= CATALOG_MAX_ENTRIES) {
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
const count = svc.columns.length;
|
|
770
|
+
lines.push(`- ${svc.title} (${count} column${count === 1 ? '' : 's'})`);
|
|
771
|
+
rendered += 1;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return { lines, rendered };
|
|
775
|
+
}
|
|
776
|
+
// Deterministic, grouped listing of a product's access points/services, so an
|
|
777
|
+
// overview answer on a large product is never cut off by the LLM's token budget.
|
|
778
|
+
export function buildAccessPointCatalog(services) {
|
|
779
|
+
const noun = services.some((s) => s.sourceType === TDSServiceSourceType.ACCESS_POINT)
|
|
780
|
+
? 'access points'
|
|
781
|
+
: 'services';
|
|
782
|
+
const { lines, rendered } = renderCatalogEntries(groupServicesByAccessPointGroup(services));
|
|
783
|
+
const omitted = services.length - rendered;
|
|
784
|
+
if (omitted > 0) {
|
|
785
|
+
lines.push(`\n- ...and ${omitted} more (ask about a specific group to see them).`);
|
|
786
|
+
}
|
|
787
|
+
const heading = omitted > 0
|
|
788
|
+
? `${rendered} of ${services.length}`
|
|
789
|
+
: `All ${services.length}`;
|
|
790
|
+
return [`## ${heading} ${noun}`, ...lines].join('\n');
|
|
791
|
+
}
|
|
792
|
+
const MAX_METADATA_PROMPT_SERVICES = 30;
|
|
793
|
+
/**
|
|
794
|
+
* Ranks the access points against the question so a product with hundreds of
|
|
795
|
+
* them contributes a bounded, relevant slice of the metadata prompt.
|
|
796
|
+
*/
|
|
797
|
+
function selectMetadataPromptServices(question, services) {
|
|
798
|
+
if (!services || services.length <= MAX_METADATA_PROMPT_SERVICES) {
|
|
799
|
+
return services;
|
|
800
|
+
}
|
|
801
|
+
return preFilterServicesByRelevance(question, services, MAX_METADATA_PROMPT_SERVICES);
|
|
802
|
+
}
|
|
693
803
|
export async function handleMetadataQuestion(question, metadata, context, startTime, hasQueryableServices, services, modelContextEnrichment) {
|
|
694
804
|
const { config, plugin, history, setMessages } = context;
|
|
695
805
|
addThinkingStep(setMessages, 'Answering from product metadata...');
|
|
696
|
-
const metadataPromptText = plugin.buildMetadataPrompt(question, metadata, history, services, modelContextEnrichment);
|
|
806
|
+
const metadataPromptText = plugin.buildMetadataPrompt(question, metadata, history, selectMetadataPromptServices(question, services), modelContextEnrichment);
|
|
697
807
|
const rawAnswer = await plugin.callLLM(metadataPromptText, config);
|
|
698
808
|
const { answer, suggestedQueries: parsedSuggestions } = parseSuggestedQueries(rawAnswer);
|
|
699
809
|
const suggestedQueries = hasQueryableServices === false && !config.orchestratorUrl
|
|
700
810
|
? []
|
|
701
811
|
: parsedSuggestions;
|
|
812
|
+
const textAnswer = services !== undefined &&
|
|
813
|
+
services.length >= CATALOG_MIN_SERVICES &&
|
|
814
|
+
isProductOverviewQuestion(question)
|
|
815
|
+
? `${answer}\n\n${buildAccessPointCatalog(services)}`
|
|
816
|
+
: answer;
|
|
702
817
|
completeThinkingSteps(setMessages);
|
|
703
818
|
updateLastAssistant(setMessages, () => ({
|
|
704
|
-
textAnswer
|
|
819
|
+
textAnswer,
|
|
705
820
|
suggestedQueries,
|
|
706
821
|
isProcessing: false,
|
|
707
822
|
thinkingDuration: elapsedSeconds(startTime),
|
|
@@ -1205,7 +1320,7 @@ async function reportOrchestratorExecutionResult(params, context) {
|
|
|
1205
1320
|
}));
|
|
1206
1321
|
}
|
|
1207
1322
|
}
|
|
1208
|
-
export async function processQuestionViaOrchestrator(question, dataProductCoordinates, metadata, context, pureExecutionContext, preResolvedEntities, modelContext) {
|
|
1323
|
+
export async function processQuestionViaOrchestrator(question, dataProductCoordinates, metadata, context, pureExecutionContext, preResolvedEntities, modelContext, priorFailure) {
|
|
1209
1324
|
const { config, plugin, setMessages } = context;
|
|
1210
1325
|
const startTime = Date.now();
|
|
1211
1326
|
try {
|
|
@@ -1214,9 +1329,26 @@ export async function processQuestionViaOrchestrator(question, dataProductCoordi
|
|
|
1214
1329
|
addThinkingStep(setMessages, `Found ${resolvedEntities.relatedEntities.length} related entities`);
|
|
1215
1330
|
}
|
|
1216
1331
|
addThinkingStep(setMessages, 'Generating Legend query via orchestrator...');
|
|
1217
|
-
const
|
|
1332
|
+
const baseContext = modelContext
|
|
1218
1333
|
? buildEnrichedBusinessContext(question, resolvedEntities.rootEntity, resolvedEntities.relatedEntities, modelContext)
|
|
1219
1334
|
: undefined;
|
|
1335
|
+
const priorFailureHints = priorFailure
|
|
1336
|
+
? buildPriorSqlFailureHints(priorFailure.failedSql, priorFailure.failedReason)
|
|
1337
|
+
: [];
|
|
1338
|
+
const enrichedContext = priorFailureHints.length > 0
|
|
1339
|
+
? {
|
|
1340
|
+
naturalLanguageQuery: question,
|
|
1341
|
+
...baseContext,
|
|
1342
|
+
businessContextMatch: {
|
|
1343
|
+
...baseContext?.businessContextMatch,
|
|
1344
|
+
additionalNlModelContext: [
|
|
1345
|
+
...(baseContext?.businessContextMatch
|
|
1346
|
+
?.additionalNlModelContext ?? []),
|
|
1347
|
+
...priorFailureHints,
|
|
1348
|
+
],
|
|
1349
|
+
},
|
|
1350
|
+
}
|
|
1351
|
+
: baseContext;
|
|
1220
1352
|
const orchestratorResponse = await withTimeout(plugin.generateQueryViaOrchestrator({
|
|
1221
1353
|
user_question: question,
|
|
1222
1354
|
semantic_search_resolution_details: {
|
|
@@ -1516,9 +1648,16 @@ export function ensureSafeLimit(sql, limit = DEFAULT_SAFETY_LIMIT) {
|
|
|
1516
1648
|
}
|
|
1517
1649
|
return `${sql.trimEnd()}\nLIMIT ${limit}`;
|
|
1518
1650
|
}
|
|
1651
|
+
/**
|
|
1652
|
+
* The one place generated SQL text is rewritten, working around engine
|
|
1653
|
+
* transpiler limits on joins. Every step no-ops on a shape it cannot fix.
|
|
1654
|
+
*/
|
|
1519
1655
|
function prepareSafeSql(sql, services) {
|
|
1520
|
-
const
|
|
1521
|
-
const
|
|
1656
|
+
const deduped = sanitizeJoinDuplicateColumns(sql, services);
|
|
1657
|
+
const ordered = sanitizeJoinOrderBy(deduped);
|
|
1658
|
+
const keyed = sanitizeJoinSameKeyColumns(ordered, services);
|
|
1659
|
+
const literals = sanitizeLiteralColumns(keyed);
|
|
1660
|
+
const wrapped = wrapBareJoinAccessPoints(literals, services);
|
|
1522
1661
|
const bounded = boundCrossAccessPointJoinDrivingSide(wrapped);
|
|
1523
1662
|
const safeSql = ensureSafeLimit(ensureDateParameters(bounded, services));
|
|
1524
1663
|
const unsupported = detectUnsupportedEnginePattern(safeSql);
|
|
@@ -1670,10 +1809,9 @@ function handleSqlGenerationFailure(setMessages, startTime, hasOrchestratorFallb
|
|
|
1670
1809
|
updateLastAssistant(setMessages, () => ({
|
|
1671
1810
|
textAnswer: orchestratorMessage,
|
|
1672
1811
|
suggestedQueries: suggestions,
|
|
1673
|
-
fallbackAction: {
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
},
|
|
1812
|
+
fallbackAction: buildOrchestratorFallbackAction({
|
|
1813
|
+
failedReason: orchestratorMessage,
|
|
1814
|
+
}),
|
|
1677
1815
|
isProcessing: false,
|
|
1678
1816
|
thinkingDuration: elapsedSeconds(startTime),
|
|
1679
1817
|
}));
|
|
@@ -1688,7 +1826,7 @@ function handleSqlGenerationFailure(setMessages, startTime, hasOrchestratorFallb
|
|
|
1688
1826
|
}
|
|
1689
1827
|
}
|
|
1690
1828
|
async function reportQueryResults(report, metadata, context, startTime, hasOrchestratorFallback) {
|
|
1691
|
-
const { currentSql, sqlResult, question, services, allAccessPoints } = report;
|
|
1829
|
+
const { currentSql, sqlResult, question, services, allAccessPoints, zeroRowExplanation, } = report;
|
|
1692
1830
|
const { config, plugin, setMessages } = context;
|
|
1693
1831
|
if (sqlResult.rows.length > 0) {
|
|
1694
1832
|
const columns = deduplicateColumns(sqlResult.columns);
|
|
@@ -1723,13 +1861,16 @@ async function reportQueryResults(report, metadata, context, startTime, hasOrche
|
|
|
1723
1861
|
}
|
|
1724
1862
|
}
|
|
1725
1863
|
else {
|
|
1726
|
-
addThinkingStep(setMessages,
|
|
1727
|
-
|
|
1864
|
+
addThinkingStep(setMessages, zeroRowExplanation === undefined
|
|
1865
|
+
? 'Query returned 0 rows after correction attempts.'
|
|
1866
|
+
: 'No overlapping join keys — the query would return 0 rows.');
|
|
1867
|
+
const joinExplanation = zeroRowExplanation ??
|
|
1868
|
+
buildCrossJoinZeroRowExplanation(currentSql, services);
|
|
1728
1869
|
const joinSuggestions = joinExplanation === undefined
|
|
1729
1870
|
? []
|
|
1730
1871
|
: buildJoinablePairSuggestions(services, allAccessPoints ?? services);
|
|
1731
1872
|
const llmAnalysis = await withTimeout(plugin
|
|
1732
|
-
.buildNoResultsFallback(question, currentSql, metadata, config, joinExplanation)
|
|
1873
|
+
.buildNoResultsFallback(question, currentSql, metadata, config, joinExplanation, services)
|
|
1733
1874
|
.catch((error) => {
|
|
1734
1875
|
assertErrorThrown(error);
|
|
1735
1876
|
return undefined;
|
|
@@ -1738,18 +1879,17 @@ async function reportQueryResults(report, metadata, context, startTime, hasOrche
|
|
|
1738
1879
|
const suggestions = joinSuggestions.length > 0
|
|
1739
1880
|
? joinSuggestions
|
|
1740
1881
|
: (llmAnalysis?.suggestedQueries ?? []);
|
|
1882
|
+
const zeroRowReason = llmAnalysis?.summary ?? joinExplanation ?? buildZeroRowMessage(services);
|
|
1741
1883
|
const fallback = hasOrchestratorFallback
|
|
1742
1884
|
? {
|
|
1743
|
-
fallbackAction: {
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
},
|
|
1885
|
+
fallbackAction: buildOrchestratorFallbackAction({
|
|
1886
|
+
failedSql: currentSql,
|
|
1887
|
+
failedReason: zeroRowReason,
|
|
1888
|
+
}),
|
|
1747
1889
|
}
|
|
1748
1890
|
: {};
|
|
1749
1891
|
updateLastAssistant(setMessages, () => ({
|
|
1750
|
-
textAnswer:
|
|
1751
|
-
joinExplanation ??
|
|
1752
|
-
buildZeroRowMessage(services),
|
|
1892
|
+
textAnswer: zeroRowReason,
|
|
1753
1893
|
...(suggestions.length > 0 ? { suggestedQueries: suggestions } : {}),
|
|
1754
1894
|
...fallback,
|
|
1755
1895
|
isProcessing: false,
|
|
@@ -1864,11 +2004,11 @@ export function applyMultiTurnBias(services, history) {
|
|
|
1864
2004
|
}
|
|
1865
2005
|
// Sort so that previously-used APs come first, preserving relative order
|
|
1866
2006
|
const biased = [...services].sort((a, b) => {
|
|
1867
|
-
const aUsed = previousAPs.has(a
|
|
2007
|
+
const aUsed = previousAPs.has(accessPointName(a).toLowerCase()) ||
|
|
1868
2008
|
previousAPs.has(a.title.toLowerCase())
|
|
1869
2009
|
? 1
|
|
1870
2010
|
: 0;
|
|
1871
|
-
const bUsed = previousAPs.has(b
|
|
2011
|
+
const bUsed = previousAPs.has(accessPointName(b).toLowerCase()) ||
|
|
1872
2012
|
previousAPs.has(b.title.toLowerCase())
|
|
1873
2013
|
? 1
|
|
1874
2014
|
: 0;
|
|
@@ -2128,43 +2268,37 @@ function buildAccessPointColumnIndex(services) {
|
|
|
2128
2268
|
}
|
|
2129
2269
|
return index;
|
|
2130
2270
|
}
|
|
2131
|
-
//
|
|
2132
|
-
//
|
|
2133
|
-
|
|
2271
|
+
// Builds a memoized distinct-value prober for a query's columns: access-point
|
|
2272
|
+
// columns resolve via a relation query, service columns via SELECT DISTINCT.
|
|
2273
|
+
export function createColumnValueProbe(services, dataProductCoordinates, fromClause, isAccessPointProbe, context, options) {
|
|
2134
2274
|
const { plugin, config, setMessages } = context;
|
|
2135
|
-
const
|
|
2136
|
-
|
|
2275
|
+
const rowLimit = options?.rowLimit ?? DISTINCT_PROBE_ROW_LIMIT;
|
|
2276
|
+
const maxColumns = options?.maxColumns ?? MAX_RECOVERED_FILTER_COLUMNS;
|
|
2137
2277
|
const accessPointByColumn = isAccessPointProbe
|
|
2138
|
-
? buildAccessPointColumnIndex(
|
|
2278
|
+
? buildAccessPointColumnIndex(services)
|
|
2139
2279
|
: new Map();
|
|
2140
|
-
const fromClause = isAccessPointProbe
|
|
2141
|
-
? undefined
|
|
2142
|
-
: extractFromClause(currentSql);
|
|
2143
|
-
if (!isAccessPointProbe && fromClause === undefined) {
|
|
2144
|
-
return undefined;
|
|
2145
|
-
}
|
|
2146
2280
|
const probeAccessPointColumn = async (column) => {
|
|
2147
2281
|
const accessPoint = accessPointByColumn.get(column.toLowerCase());
|
|
2148
2282
|
const pId = accessPoint ? servicePId(accessPoint) : undefined;
|
|
2149
2283
|
if (pId === undefined) {
|
|
2150
2284
|
return { columns: [], rows: [] };
|
|
2151
2285
|
}
|
|
2152
|
-
const relationQuery = `#P{${pId}}#->select(~[${pureRelationColumnRef(column)}])->distinct()->take(${
|
|
2286
|
+
const relationQuery = `#P{${pId}}#->select(~[${pureRelationColumnRef(column)}])->distinct()->take(${rowLimit})`;
|
|
2153
2287
|
return plugin.executeLakehouseRelationQuery(relationQuery, guaranteeNonNullable(dataProductCoordinates), config);
|
|
2154
2288
|
};
|
|
2155
2289
|
const probedValues = new Map();
|
|
2156
|
-
|
|
2290
|
+
return async (column) => {
|
|
2157
2291
|
if (probedValues.has(column)) {
|
|
2158
2292
|
return probedValues.get(column);
|
|
2159
2293
|
}
|
|
2160
|
-
if (probedValues.size >=
|
|
2294
|
+
if (probedValues.size >= maxColumns) {
|
|
2161
2295
|
return undefined;
|
|
2162
2296
|
}
|
|
2163
2297
|
let values;
|
|
2164
2298
|
try {
|
|
2165
2299
|
const probe = isAccessPointProbe
|
|
2166
2300
|
? await probeAccessPointColumn(column)
|
|
2167
|
-
: await executeSqlForServices(`SELECT DISTINCT "${column}" ${fromClause} LIMIT ${
|
|
2301
|
+
: await executeSqlForServices(`SELECT DISTINCT "${column}" ${fromClause} LIMIT ${rowLimit}`, services, dataProductCoordinates, plugin, config);
|
|
2168
2302
|
values = probe.rows
|
|
2169
2303
|
.map((row) => row[column])
|
|
2170
2304
|
.filter(isNonNullable)
|
|
@@ -2172,12 +2306,92 @@ async function attemptDistinctValueRecovery(currentSql, selectedServices, dataPr
|
|
|
2172
2306
|
}
|
|
2173
2307
|
catch (probeError) {
|
|
2174
2308
|
assertErrorThrown(probeError);
|
|
2175
|
-
|
|
2309
|
+
if (options?.silent !== true) {
|
|
2310
|
+
addThinkingStep(setMessages, `Could not probe values for "${column}"`);
|
|
2311
|
+
}
|
|
2176
2312
|
values = undefined;
|
|
2177
2313
|
}
|
|
2178
2314
|
probedValues.set(column, values);
|
|
2179
2315
|
return values;
|
|
2180
2316
|
};
|
|
2317
|
+
}
|
|
2318
|
+
// An empty inner join yields no rows, but an outer join, an aggregate, a
|
|
2319
|
+
// disjunctive predicate or a set operation can still return some.
|
|
2320
|
+
export function canSkipExecutionOnEmptyJoin(sql) {
|
|
2321
|
+
return (!OUTER_JOIN_PATTERN.test(sql) &&
|
|
2322
|
+
!HAS_AGGREGATION_PATTERN.test(sql) &&
|
|
2323
|
+
!DISJUNCTIVE_PREDICATE_PATTERN.test(sql) &&
|
|
2324
|
+
!SET_OPERATION_PATTERN.test(sql));
|
|
2325
|
+
}
|
|
2326
|
+
/**
|
|
2327
|
+
* Reports the join key whose values do not overlap, so a two access point join
|
|
2328
|
+
* can be explained instead of run. Text keys only, because dates and numbers
|
|
2329
|
+
* serialize differently on either side and would read as disjoint when they
|
|
2330
|
+
* are not, and only for exactly two access points, because a key belonging to
|
|
2331
|
+
* another pair would otherwise be tested against the wrong relations.
|
|
2332
|
+
*/
|
|
2333
|
+
export async function probeJoinOverlap(sql, services, joinKeys, dataProductCoordinates, context) {
|
|
2334
|
+
if (dataProductCoordinates === undefined) {
|
|
2335
|
+
return undefined;
|
|
2336
|
+
}
|
|
2337
|
+
const involved = resolveJoinedAccessPoints(sql, services);
|
|
2338
|
+
const apA = involved[0];
|
|
2339
|
+
const apB = involved[1];
|
|
2340
|
+
if (involved.length !== 2 || apA === undefined || apB === undefined) {
|
|
2341
|
+
return undefined;
|
|
2342
|
+
}
|
|
2343
|
+
const sharedKeys = sharedColumnNames(apA, apB)
|
|
2344
|
+
.filter((name) => joinKeys.has(name.toLowerCase()))
|
|
2345
|
+
.slice(0, MAX_JOIN_OVERLAP_PROBE_KEYS);
|
|
2346
|
+
if (sharedKeys.length === 0) {
|
|
2347
|
+
return undefined;
|
|
2348
|
+
}
|
|
2349
|
+
const columnsA = buildColumnByNameIndex(apA.columns);
|
|
2350
|
+
const columnsB = buildColumnByNameIndex(apB.columns);
|
|
2351
|
+
const probeOptions = {
|
|
2352
|
+
rowLimit: JOIN_OVERLAP_PROBE_LIMIT,
|
|
2353
|
+
maxColumns: MAX_JOIN_OVERLAP_PROBE_KEYS,
|
|
2354
|
+
silent: true,
|
|
2355
|
+
};
|
|
2356
|
+
const probeA = createColumnValueProbe([apA], dataProductCoordinates, undefined, true, context, probeOptions);
|
|
2357
|
+
const probeB = createColumnValueProbe([apB], dataProductCoordinates, undefined, true, context, probeOptions);
|
|
2358
|
+
const fullyEnumerated = (values) => values !== undefined &&
|
|
2359
|
+
values.length > 0 &&
|
|
2360
|
+
values.length < JOIN_OVERLAP_CONCLUSIVE_MAX;
|
|
2361
|
+
for (const key of sharedKeys) {
|
|
2362
|
+
const columnA = columnsA.get(key.toLowerCase());
|
|
2363
|
+
const columnB = columnsB.get(key.toLowerCase());
|
|
2364
|
+
if (columnA === undefined || columnB === undefined) {
|
|
2365
|
+
continue;
|
|
2366
|
+
}
|
|
2367
|
+
const nameB = columnB.name;
|
|
2368
|
+
if (!isStringTypedColumn(columnA) || !isStringTypedColumn(columnB)) {
|
|
2369
|
+
continue;
|
|
2370
|
+
}
|
|
2371
|
+
const [valuesA, valuesB] = await Promise.all([probeA(key), probeB(nameB)]);
|
|
2372
|
+
if (!fullyEnumerated(valuesA) || !fullyEnumerated(valuesB)) {
|
|
2373
|
+
continue;
|
|
2374
|
+
}
|
|
2375
|
+
const setB = new Set(valuesB.map((v) => v.toLowerCase()));
|
|
2376
|
+
if (!valuesA.some((v) => setB.has(v.toLowerCase()))) {
|
|
2377
|
+
return buildDisjointJoinMessage(apA.title, apB.title, key, previewValues(valuesA), previewValues(valuesB));
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
return undefined;
|
|
2381
|
+
}
|
|
2382
|
+
// Recovers a zero-row query by re-probing each WHERE-filter column's real distinct
|
|
2383
|
+
// values and rewriting equality/IN literals that don't match (casing/spelling).
|
|
2384
|
+
async function attemptDistinctValueRecovery(currentSql, selectedServices, dataProductCoordinates, context) {
|
|
2385
|
+
const { plugin, config, setMessages } = context;
|
|
2386
|
+
const isAccessPointProbe = dataProductCoordinates !== undefined &&
|
|
2387
|
+
selectedServices.some((s) => s.sourceType === TDSServiceSourceType.ACCESS_POINT);
|
|
2388
|
+
const fromClause = isAccessPointProbe
|
|
2389
|
+
? undefined
|
|
2390
|
+
: extractFromClause(currentSql);
|
|
2391
|
+
if (!isAccessPointProbe && fromClause === undefined) {
|
|
2392
|
+
return undefined;
|
|
2393
|
+
}
|
|
2394
|
+
const probeColumnValues = createColumnValueProbe(selectedServices, dataProductCoordinates, fromClause, isAccessPointProbe, context);
|
|
2181
2395
|
await Promise.all(collectWhereFilterColumns(currentSql)
|
|
2182
2396
|
.slice(0, MAX_RECOVERED_FILTER_COLUMNS)
|
|
2183
2397
|
.map((column) => probeColumnValues(column)));
|
|
@@ -2343,14 +2557,14 @@ async function processAccessPointQuery(question, accessPoints, metadata, context
|
|
|
2343
2557
|
const { config, plugin, setMessages } = context;
|
|
2344
2558
|
addThinkingStep(setMessages, 'Found relevant access points to query');
|
|
2345
2559
|
const selectedAPs = await selectBestServices(question, accessPoints, context);
|
|
2346
|
-
const currentAccessPoints = new Set(selectedAPs.map((ap) => ap
|
|
2560
|
+
const currentAccessPoints = new Set(selectedAPs.map((ap) => accessPointName(ap)));
|
|
2347
2561
|
const scopedContext = {
|
|
2348
2562
|
...context,
|
|
2349
2563
|
history: filterHistoryForAccessPoints(context.history, currentAccessPoints),
|
|
2350
2564
|
};
|
|
2351
2565
|
try {
|
|
2352
2566
|
addThinkingStep(setMessages, 'Grounding filter values from real data...');
|
|
2353
|
-
await withTimeout(plugin.enrichAccessPointSampleValues(selectedAPs, dataProductCoordinates, config), VALUE_GROUNDING_TIME_BUDGET_MS);
|
|
2567
|
+
await withTimeout(plugin.enrichAccessPointSampleValues(selectedAPs, dataProductCoordinates, config, question), VALUE_GROUNDING_TIME_BUDGET_MS);
|
|
2354
2568
|
}
|
|
2355
2569
|
catch (error) {
|
|
2356
2570
|
assertErrorThrown(error);
|
|
@@ -2368,9 +2582,34 @@ async function processAccessPointQuery(question, accessPoints, metadata, context
|
|
|
2368
2582
|
updateLastAssistant(setMessages, () => ({
|
|
2369
2583
|
sql: finalSql,
|
|
2370
2584
|
sqlGenTime: sqlGenTimeValue,
|
|
2371
|
-
isExecuting: true,
|
|
2372
2585
|
queriedAccessPoints: orderedAccessPointsFromSql(finalSql),
|
|
2373
2586
|
}));
|
|
2587
|
+
const joinKeys = canSkipExecutionOnEmptyJoin(finalSql)
|
|
2588
|
+
? extractJoinKeyColumns(finalSql)
|
|
2589
|
+
: new Set();
|
|
2590
|
+
let disjointJoin = joinKeys.size > 0
|
|
2591
|
+
? detectDisjointJoinUniverses(finalSql, selectedAPs, {
|
|
2592
|
+
requireComplete: true,
|
|
2593
|
+
restrictToColumns: joinKeys,
|
|
2594
|
+
})
|
|
2595
|
+
: undefined;
|
|
2596
|
+
if (disjointJoin === undefined && joinKeys.size > 0) {
|
|
2597
|
+
addThinkingStep(setMessages, 'Probing join-key overlap...');
|
|
2598
|
+
disjointJoin = await withTimeout(probeJoinOverlap(finalSql, selectedAPs, joinKeys, dataProductCoordinates, scopedContext), JOIN_OVERLAP_PROBE_TIMEOUT_MS);
|
|
2599
|
+
}
|
|
2600
|
+
if (disjointJoin !== undefined) {
|
|
2601
|
+
addThinkingStep(setMessages, 'Join keys do not overlap — skipping execution');
|
|
2602
|
+
await reportQueryResults({
|
|
2603
|
+
currentSql: finalSql,
|
|
2604
|
+
sqlResult: { columns: [], rows: [] },
|
|
2605
|
+
question,
|
|
2606
|
+
services: selectedAPs,
|
|
2607
|
+
allAccessPoints: accessPoints,
|
|
2608
|
+
zeroRowExplanation: disjointJoin,
|
|
2609
|
+
}, metadata, scopedContext, startTime, false);
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
updateLastAssistant(setMessages, () => ({ isExecuting: true }));
|
|
2374
2613
|
const execStartTime = Date.now();
|
|
2375
2614
|
try {
|
|
2376
2615
|
let execSql = finalSql;
|
|
@@ -2428,13 +2667,22 @@ async function processDataQuery(question, services, coordinates, metadata, conte
|
|
|
2428
2667
|
const missingParams = detectMissingServiceParams(judgedSql, selectedServices);
|
|
2429
2668
|
if (missingParams.length > 0) {
|
|
2430
2669
|
const sqlGenTimeValue = elapsedSeconds(startTime, 2);
|
|
2670
|
+
const missingParamsReason = `Missing required parameter${missingParams.length > 1 ? 's' : ''}: ${missingParams.map((p) => p.name).join(', ')}`;
|
|
2431
2671
|
completeThinkingSteps(setMessages);
|
|
2432
|
-
addThinkingStep(setMessages,
|
|
2672
|
+
addThinkingStep(setMessages, missingParamsReason);
|
|
2433
2673
|
completeThinkingSteps(setMessages);
|
|
2434
2674
|
updateLastAssistant(setMessages, () => ({
|
|
2435
2675
|
sql: judgedSql,
|
|
2436
2676
|
sqlGenTime: sqlGenTimeValue,
|
|
2437
2677
|
textAnswer: buildMissingParamsWarning(missingParams),
|
|
2678
|
+
...(hasOrchestratorFallback
|
|
2679
|
+
? {
|
|
2680
|
+
fallbackAction: buildOrchestratorFallbackAction({
|
|
2681
|
+
failedSql: judgedSql,
|
|
2682
|
+
failedReason: missingParamsReason,
|
|
2683
|
+
}),
|
|
2684
|
+
}
|
|
2685
|
+
: {}),
|
|
2438
2686
|
isProcessing: false,
|
|
2439
2687
|
isExecuting: false,
|
|
2440
2688
|
thinkingDuration: elapsedSeconds(startTime),
|
|
@@ -2450,9 +2698,10 @@ async function processDataQuery(question, services, coordinates, metadata, conte
|
|
|
2450
2698
|
}));
|
|
2451
2699
|
const execOutcome = await executeSqlWithRetries(judgedSql, question, selectedServices, coordinates, dataProductCoordinates, context);
|
|
2452
2700
|
if (execOutcome.error) {
|
|
2453
|
-
const
|
|
2454
|
-
|
|
2455
|
-
|
|
2701
|
+
const execError = execOutcome.error;
|
|
2702
|
+
const execErrorType = classifyError(new Error(execError));
|
|
2703
|
+
addThinkingStep(setMessages, `Execution failed: ${execError.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
2704
|
+
finishWithThinkingError(setMessages, buildExecutionErrorMessage(execError, selectedServices), startTime, execErrorType === LegendAIErrorType.GENERAL
|
|
2456
2705
|
? LegendAIErrorType.EXECUTION
|
|
2457
2706
|
: execErrorType);
|
|
2458
2707
|
updateLastAssistant(setMessages, () => ({
|
|
@@ -2460,10 +2709,10 @@ async function processDataQuery(question, services, coordinates, metadata, conte
|
|
|
2460
2709
|
suggestedQueries: buildFallbackSuggestions(selectedServices),
|
|
2461
2710
|
...(hasOrchestratorFallback
|
|
2462
2711
|
? {
|
|
2463
|
-
fallbackAction: {
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
},
|
|
2712
|
+
fallbackAction: buildOrchestratorFallbackAction({
|
|
2713
|
+
failedSql: execOutcome.sql,
|
|
2714
|
+
failedReason: execError,
|
|
2715
|
+
}),
|
|
2467
2716
|
}
|
|
2468
2717
|
: {}),
|
|
2469
2718
|
}));
|
|
@@ -2549,10 +2798,7 @@ export async function processQuestion(question, services, coordinates, metadata,
|
|
|
2549
2798
|
await handleMetadataQuestion(question, metadata, context, startTime, false, services, getMetadataEnrichment());
|
|
2550
2799
|
if (config.orchestratorUrl && dataProductCoordinates) {
|
|
2551
2800
|
updateLastAssistant(setMessages, () => ({
|
|
2552
|
-
fallbackAction:
|
|
2553
|
-
label: ORCHESTRATOR_FALLBACK_LABEL,
|
|
2554
|
-
actionId: LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
|
|
2555
|
-
},
|
|
2801
|
+
fallbackAction: buildOrchestratorFallbackAction(),
|
|
2556
2802
|
}));
|
|
2557
2803
|
}
|
|
2558
2804
|
return;
|
|
@@ -2625,10 +2871,7 @@ export async function processQuestionWithIntent(question, intent, services, coor
|
|
|
2625
2871
|
const startTime = Date.now();
|
|
2626
2872
|
await handleMetadataQuestion(question, metadata, context, startTime, false, services, getMetadataEnrichment());
|
|
2627
2873
|
updateLastAssistant(setMessages, () => ({
|
|
2628
|
-
fallbackAction:
|
|
2629
|
-
label: ORCHESTRATOR_FALLBACK_LABEL,
|
|
2630
|
-
actionId: LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
|
|
2631
|
-
},
|
|
2874
|
+
fallbackAction: buildOrchestratorFallbackAction(),
|
|
2632
2875
|
}));
|
|
2633
2876
|
return;
|
|
2634
2877
|
}
|