@finos/legend-lego 2.0.217 → 2.0.218
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
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
uuid,
|
|
23
23
|
} from '@finos/legend-shared';
|
|
24
24
|
import {
|
|
25
|
+
type LegendAIFallbackAction,
|
|
26
|
+
type LegendAIPriorSqlFailure,
|
|
25
27
|
type TDSServiceSchema,
|
|
26
28
|
type LegendAIConfig,
|
|
27
29
|
type LegendAIAssistantMessage,
|
|
@@ -68,6 +70,7 @@ import {
|
|
|
68
70
|
relaxExactStringFilters,
|
|
69
71
|
extractFilteredColumns,
|
|
70
72
|
buildProbedValueHints,
|
|
73
|
+
buildPriorSqlFailureHints,
|
|
71
74
|
splitIdentifierTokens,
|
|
72
75
|
tokenizeText,
|
|
73
76
|
} from '../LegendAIDocEnrichment.js';
|
|
@@ -78,16 +81,25 @@ import {
|
|
|
78
81
|
import {
|
|
79
82
|
isNumericColumn,
|
|
80
83
|
isStringColumn,
|
|
84
|
+
isStringTypedColumn,
|
|
81
85
|
} from '../components/LegendAIChatHelpers.js';
|
|
82
86
|
import {
|
|
87
|
+
buildColumnByNameIndex,
|
|
88
|
+
accessPointName,
|
|
83
89
|
HAS_LIMIT_PATTERN,
|
|
84
|
-
|
|
90
|
+
accessPointCalls,
|
|
85
91
|
servicePId,
|
|
86
92
|
pureRelationColumnRef,
|
|
93
|
+
extractJoinKeyColumns,
|
|
94
|
+
sharedColumnNames,
|
|
87
95
|
} from './LegendAISqlHelpers.js';
|
|
88
96
|
import {
|
|
89
97
|
buildCrossJoinZeroRowExplanation,
|
|
90
98
|
buildJoinablePairSuggestions,
|
|
99
|
+
detectDisjointJoinUniverses,
|
|
100
|
+
resolveJoinedAccessPoints,
|
|
101
|
+
buildDisjointJoinMessage,
|
|
102
|
+
previewValues,
|
|
91
103
|
} from './LegendAIJoinAnalysis.js';
|
|
92
104
|
import {
|
|
93
105
|
boundCrossAccessPointJoinDrivingSide,
|
|
@@ -106,6 +118,10 @@ const ANALYSIS_TIMEOUT_MS = 15_000;
|
|
|
106
118
|
const ORCHESTRATOR_GENERATION_TIMEOUT_MS = 120_000;
|
|
107
119
|
const DISTINCT_PROBE_ROW_LIMIT = 5000;
|
|
108
120
|
const MAX_RECOVERED_FILTER_COLUMNS = 4;
|
|
121
|
+
export const JOIN_OVERLAP_PROBE_LIMIT = 200;
|
|
122
|
+
const JOIN_OVERLAP_CONCLUSIVE_MAX = JOIN_OVERLAP_PROBE_LIMIT - 1;
|
|
123
|
+
const MAX_JOIN_OVERLAP_PROBE_KEYS = 4;
|
|
124
|
+
const JOIN_OVERLAP_PROBE_TIMEOUT_MS = 15_000;
|
|
109
125
|
const EXECUTION_TIMEOUT_MS = 300_000;
|
|
110
126
|
const ANALYSIS_PREVIEW_ROW_LIMIT = 3;
|
|
111
127
|
const ANALYSIS_PREVIEW_VALUE_LIMIT = 40;
|
|
@@ -117,12 +133,32 @@ const SERVICE_CALL_PATTERN = /\bservice\s*\([^()]*\)/gi;
|
|
|
117
133
|
const DEFAULT_SAFETY_LIMIT = 1000;
|
|
118
134
|
const HAS_AGGREGATION_PATTERN =
|
|
119
135
|
/\bGROUP\s+BY\b|\b(?:COUNT|SUM|AVG|MIN|MAX)\s*\(|\bSELECT\s+DISTINCT\b|\bHAVING\b|\bQUALIFY\b/i;
|
|
136
|
+
const OUTER_JOIN_PATTERN = /\b(?:LEFT|RIGHT|FULL)\s+(?:OUTER\s+)?JOIN\b/i;
|
|
137
|
+
const DISJUNCTIVE_PREDICATE_PATTERN = /\bOR\b/i;
|
|
138
|
+
const SET_OPERATION_PATTERN = /\b(?:UNION|INTERSECT|EXCEPT)\b/i;
|
|
120
139
|
const MAX_SERVICES_FOR_LLM_SELECTION = 30;
|
|
121
140
|
const VALUE_GROUNDING_TIME_BUDGET_MS = 4000;
|
|
122
141
|
const AP_SQL_EXECUTION_TIMEOUT_MS = 90_000;
|
|
123
142
|
const AP_SQL_EXECUTION_TIMEOUT_MESSAGE =
|
|
124
143
|
'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.';
|
|
125
144
|
const ORCHESTRATOR_FALLBACK_LABEL = 'Try Legend AI Orchestrator';
|
|
145
|
+
|
|
146
|
+
// The single orchestrator fallback action, so every dead-end path offers the
|
|
147
|
+
// same button and threads any prior SQL failure the same way.
|
|
148
|
+
export function buildOrchestratorFallbackAction(
|
|
149
|
+
failure?: LegendAIPriorSqlFailure,
|
|
150
|
+
): LegendAIFallbackAction {
|
|
151
|
+
return {
|
|
152
|
+
label: ORCHESTRATOR_FALLBACK_LABEL,
|
|
153
|
+
actionId: LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
|
|
154
|
+
...(failure?.failedSql === undefined
|
|
155
|
+
? {}
|
|
156
|
+
: { failedSql: failure.failedSql }),
|
|
157
|
+
...(failure?.failedReason === undefined
|
|
158
|
+
? {}
|
|
159
|
+
: { failedReason: failure.failedReason }),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
126
162
|
const SQL_GENERATION_FAILURE_WITH_ORCHESTRATOR =
|
|
127
163
|
'SQL generation could not handle this query. You can try the Legend AI Orchestrator to generate a Pure query instead.';
|
|
128
164
|
const SQL_GENERATION_FAILURE_NO_ORCHESTRATOR =
|
|
@@ -186,31 +222,35 @@ export type MessageSetter = React.Dispatch<
|
|
|
186
222
|
React.SetStateAction<LegendAIMessage[]>
|
|
187
223
|
>;
|
|
188
224
|
|
|
225
|
+
export function createAssistantMessage(): LegendAIAssistantMessage {
|
|
226
|
+
return {
|
|
227
|
+
id: uuid(),
|
|
228
|
+
role: LegendAIMessageRole.ASSISTANT,
|
|
229
|
+
thinkingSteps: [],
|
|
230
|
+
sql: null,
|
|
231
|
+
textAnswer: null,
|
|
232
|
+
dataContext: null,
|
|
233
|
+
gridData: null,
|
|
234
|
+
error: null,
|
|
235
|
+
errorType: null,
|
|
236
|
+
sqlGenTime: null,
|
|
237
|
+
execTime: null,
|
|
238
|
+
thinkingDuration: null,
|
|
239
|
+
isProcessing: true,
|
|
240
|
+
isExecuting: false,
|
|
241
|
+
suggestedQueries: [],
|
|
242
|
+
fallbackAction: null,
|
|
243
|
+
queriedAccessPointGroups: [],
|
|
244
|
+
queriedAccessPoints: [],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
189
248
|
export function createMessagePair(
|
|
190
249
|
text: string,
|
|
191
250
|
): [LegendAIUserMessage, LegendAIAssistantMessage] {
|
|
192
251
|
return [
|
|
193
252
|
{ id: uuid(), role: LegendAIMessageRole.USER, text },
|
|
194
|
-
|
|
195
|
-
id: uuid(),
|
|
196
|
-
role: LegendAIMessageRole.ASSISTANT,
|
|
197
|
-
thinkingSteps: [],
|
|
198
|
-
sql: null,
|
|
199
|
-
textAnswer: null,
|
|
200
|
-
dataContext: null,
|
|
201
|
-
gridData: null,
|
|
202
|
-
error: null,
|
|
203
|
-
errorType: null,
|
|
204
|
-
sqlGenTime: null,
|
|
205
|
-
execTime: null,
|
|
206
|
-
thinkingDuration: null,
|
|
207
|
-
isProcessing: true,
|
|
208
|
-
isExecuting: false,
|
|
209
|
-
suggestedQueries: [],
|
|
210
|
-
fallbackAction: null,
|
|
211
|
-
queriedAccessPointGroups: [],
|
|
212
|
-
queriedAccessPoints: [],
|
|
213
|
-
},
|
|
253
|
+
createAssistantMessage(),
|
|
214
254
|
];
|
|
215
255
|
}
|
|
216
256
|
|
|
@@ -385,7 +425,7 @@ export function filterHistoryForAccessPoints(
|
|
|
385
425
|
function orderedAccessPointsFromSql(sql: string): string[] {
|
|
386
426
|
const ordered: string[] = [];
|
|
387
427
|
const seen = new Set<string>();
|
|
388
|
-
for (const match of sql
|
|
428
|
+
for (const match of accessPointCalls(sql)) {
|
|
389
429
|
const path = match.groups?.pId ?? '';
|
|
390
430
|
const dotIdx = path.lastIndexOf('.');
|
|
391
431
|
const apName = dotIdx === -1 ? path : path.slice(dotIdx + 1);
|
|
@@ -397,6 +437,9 @@ function orderedAccessPointsFromSql(sql: string): string[] {
|
|
|
397
437
|
return ordered;
|
|
398
438
|
}
|
|
399
439
|
|
|
440
|
+
const MAX_HISTORY_ANSWER_LENGTH = 1500;
|
|
441
|
+
const MAX_CONVERSATION_HISTORY_TURNS = 10;
|
|
442
|
+
|
|
400
443
|
function buildTurnFromAssistant(
|
|
401
444
|
userText: string,
|
|
402
445
|
asstMsg: LegendAIAssistantMessage,
|
|
@@ -426,7 +469,7 @@ function buildTurnFromAssistant(
|
|
|
426
469
|
if (asstMsg.textAnswer) {
|
|
427
470
|
return {
|
|
428
471
|
question: userText,
|
|
429
|
-
sql: asstMsg.textAnswer,
|
|
472
|
+
sql: asstMsg.textAnswer.slice(0, MAX_HISTORY_ANSWER_LENGTH),
|
|
430
473
|
intent: LegendAIQuestionIntent.METADATA,
|
|
431
474
|
};
|
|
432
475
|
}
|
|
@@ -465,6 +508,10 @@ export function classifyResponseOutcome(
|
|
|
465
508
|
return LegendAIResponseOutcome.NO_ANSWER;
|
|
466
509
|
}
|
|
467
510
|
|
|
511
|
+
/**
|
|
512
|
+
* Pairs each question with the assistant turn that answered it, keeping only
|
|
513
|
+
* the most recent turns so prompts do not grow with conversation length.
|
|
514
|
+
*/
|
|
468
515
|
export function buildConversationHistory(
|
|
469
516
|
messages: LegendAIMessage[],
|
|
470
517
|
): LegendAIConversationTurn[] {
|
|
@@ -486,7 +533,7 @@ export function buildConversationHistory(
|
|
|
486
533
|
i += 1;
|
|
487
534
|
}
|
|
488
535
|
}
|
|
489
|
-
return history;
|
|
536
|
+
return history.slice(-MAX_CONVERSATION_HISTORY_TURNS);
|
|
490
537
|
}
|
|
491
538
|
|
|
492
539
|
function formatServiceParams(services: TDSServiceSchema[]): string[] {
|
|
@@ -968,6 +1015,118 @@ export function attachMetadataOverview(
|
|
|
968
1015
|
});
|
|
969
1016
|
}
|
|
970
1017
|
|
|
1018
|
+
const PRODUCT_OVERVIEW_PATTERNS: readonly RegExp[] = [
|
|
1019
|
+
/\bwhat\s+does\b.*\b(?:offer|provide|contain|have|include|cover)\b/,
|
|
1020
|
+
/\b(?:describe|summarize|overview|summary)\b/,
|
|
1021
|
+
/\bwhat\s+(?:access\s*points?|services?|data|datasets?)\b/,
|
|
1022
|
+
/\bwhat\s+can\s+(?:i|we|you)\b/,
|
|
1023
|
+
/\btell\s+me\s+(?:about|more)\b/,
|
|
1024
|
+
];
|
|
1025
|
+
|
|
1026
|
+
const LISTING_VERB_PATTERN = /\b(?:list|show|what\s+are)\b/;
|
|
1027
|
+
const LISTING_NOUN_PATTERN =
|
|
1028
|
+
/\b(?:access\s*points?|services?|endpoints?|datasets?|tables?)\b/;
|
|
1029
|
+
|
|
1030
|
+
export function isProductOverviewQuestion(question: string): boolean {
|
|
1031
|
+
const q = question.toLowerCase();
|
|
1032
|
+
return (
|
|
1033
|
+
PRODUCT_OVERVIEW_PATTERNS.some((pattern) => pattern.test(q)) ||
|
|
1034
|
+
(LISTING_VERB_PATTERN.test(q) && LISTING_NOUN_PATTERN.test(q))
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
const CATALOG_MIN_SERVICES = 10;
|
|
1039
|
+
const CATALOG_MAX_ENTRIES = 200;
|
|
1040
|
+
|
|
1041
|
+
// Buckets services under their access point group, groups in title order.
|
|
1042
|
+
function groupServicesByAccessPointGroup(
|
|
1043
|
+
services: TDSServiceSchema[],
|
|
1044
|
+
): [string, TDSServiceSchema[]][] {
|
|
1045
|
+
const byGroup = new Map<string, TDSServiceSchema[]>();
|
|
1046
|
+
for (const svc of services) {
|
|
1047
|
+
const group = svc.accessPointGroupTitle ?? '';
|
|
1048
|
+
const existing = byGroup.get(group);
|
|
1049
|
+
if (existing) {
|
|
1050
|
+
existing.push(svc);
|
|
1051
|
+
} else {
|
|
1052
|
+
byGroup.set(group, [svc]);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
return Array.from(byGroup.entries()).sort((a, b) => a[0].localeCompare(b[0]));
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// Renders each group as a heading followed by its services, up to the cap.
|
|
1059
|
+
function renderCatalogEntries(groups: [string, TDSServiceSchema[]][]): {
|
|
1060
|
+
lines: string[];
|
|
1061
|
+
rendered: number;
|
|
1062
|
+
} {
|
|
1063
|
+
const lines: string[] = [];
|
|
1064
|
+
let rendered = 0;
|
|
1065
|
+
for (const [group, entries] of groups) {
|
|
1066
|
+
if (rendered >= CATALOG_MAX_ENTRIES) {
|
|
1067
|
+
break;
|
|
1068
|
+
}
|
|
1069
|
+
if (group) {
|
|
1070
|
+
lines.push(`\n### ${group}`);
|
|
1071
|
+
}
|
|
1072
|
+
for (const svc of [...entries].sort((a, b) =>
|
|
1073
|
+
a.title.localeCompare(b.title),
|
|
1074
|
+
)) {
|
|
1075
|
+
if (rendered >= CATALOG_MAX_ENTRIES) {
|
|
1076
|
+
break;
|
|
1077
|
+
}
|
|
1078
|
+
const count = svc.columns.length;
|
|
1079
|
+
lines.push(`- ${svc.title} (${count} column${count === 1 ? '' : 's'})`);
|
|
1080
|
+
rendered += 1;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
return { lines, rendered };
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// Deterministic, grouped listing of a product's access points/services, so an
|
|
1087
|
+
// overview answer on a large product is never cut off by the LLM's token budget.
|
|
1088
|
+
export function buildAccessPointCatalog(services: TDSServiceSchema[]): string {
|
|
1089
|
+
const noun = services.some(
|
|
1090
|
+
(s) => s.sourceType === TDSServiceSourceType.ACCESS_POINT,
|
|
1091
|
+
)
|
|
1092
|
+
? 'access points'
|
|
1093
|
+
: 'services';
|
|
1094
|
+
const { lines, rendered } = renderCatalogEntries(
|
|
1095
|
+
groupServicesByAccessPointGroup(services),
|
|
1096
|
+
);
|
|
1097
|
+
const omitted = services.length - rendered;
|
|
1098
|
+
if (omitted > 0) {
|
|
1099
|
+
lines.push(
|
|
1100
|
+
`\n- ...and ${omitted} more (ask about a specific group to see them).`,
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
const heading =
|
|
1104
|
+
omitted > 0
|
|
1105
|
+
? `${rendered} of ${services.length}`
|
|
1106
|
+
: `All ${services.length}`;
|
|
1107
|
+
return [`## ${heading} ${noun}`, ...lines].join('\n');
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
const MAX_METADATA_PROMPT_SERVICES = 30;
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* Ranks the access points against the question so a product with hundreds of
|
|
1114
|
+
* them contributes a bounded, relevant slice of the metadata prompt.
|
|
1115
|
+
*/
|
|
1116
|
+
function selectMetadataPromptServices(
|
|
1117
|
+
question: string,
|
|
1118
|
+
services: TDSServiceSchema[] | undefined,
|
|
1119
|
+
): TDSServiceSchema[] | undefined {
|
|
1120
|
+
if (!services || services.length <= MAX_METADATA_PROMPT_SERVICES) {
|
|
1121
|
+
return services;
|
|
1122
|
+
}
|
|
1123
|
+
return preFilterServicesByRelevance(
|
|
1124
|
+
question,
|
|
1125
|
+
services,
|
|
1126
|
+
MAX_METADATA_PROMPT_SERVICES,
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
971
1130
|
export async function handleMetadataQuestion(
|
|
972
1131
|
question: string,
|
|
973
1132
|
metadata: LegendAIProductMetadata,
|
|
@@ -983,7 +1142,7 @@ export async function handleMetadataQuestion(
|
|
|
983
1142
|
question,
|
|
984
1143
|
metadata,
|
|
985
1144
|
history,
|
|
986
|
-
services,
|
|
1145
|
+
selectMetadataPromptServices(question, services),
|
|
987
1146
|
modelContextEnrichment,
|
|
988
1147
|
);
|
|
989
1148
|
const rawAnswer = await plugin.callLLM(metadataPromptText, config);
|
|
@@ -993,9 +1152,15 @@ export async function handleMetadataQuestion(
|
|
|
993
1152
|
hasQueryableServices === false && !config.orchestratorUrl
|
|
994
1153
|
? []
|
|
995
1154
|
: parsedSuggestions;
|
|
1155
|
+
const textAnswer =
|
|
1156
|
+
services !== undefined &&
|
|
1157
|
+
services.length >= CATALOG_MIN_SERVICES &&
|
|
1158
|
+
isProductOverviewQuestion(question)
|
|
1159
|
+
? `${answer}\n\n${buildAccessPointCatalog(services)}`
|
|
1160
|
+
: answer;
|
|
996
1161
|
completeThinkingSteps(setMessages);
|
|
997
1162
|
updateLastAssistant(setMessages, () => ({
|
|
998
|
-
textAnswer
|
|
1163
|
+
textAnswer,
|
|
999
1164
|
suggestedQueries,
|
|
1000
1165
|
isProcessing: false,
|
|
1001
1166
|
thinkingDuration: elapsedSeconds(startTime),
|
|
@@ -2025,6 +2190,7 @@ export async function processQuestionViaOrchestrator(
|
|
|
2025
2190
|
pureExecutionContext?: QueryExplicitExecutionContextInfo,
|
|
2026
2191
|
preResolvedEntities?: LegendAIResolvedEntities,
|
|
2027
2192
|
modelContext?: LegendAIModelContext,
|
|
2193
|
+
priorFailure?: LegendAIPriorSqlFailure,
|
|
2028
2194
|
): Promise<void> {
|
|
2029
2195
|
const { config, plugin, setMessages } = context;
|
|
2030
2196
|
const startTime = Date.now();
|
|
@@ -2047,7 +2213,7 @@ export async function processQuestionViaOrchestrator(
|
|
|
2047
2213
|
}
|
|
2048
2214
|
|
|
2049
2215
|
addThinkingStep(setMessages, 'Generating Legend query via orchestrator...');
|
|
2050
|
-
const
|
|
2216
|
+
const baseContext = modelContext
|
|
2051
2217
|
? buildEnrichedBusinessContext(
|
|
2052
2218
|
question,
|
|
2053
2219
|
resolvedEntities.rootEntity,
|
|
@@ -2055,6 +2221,27 @@ export async function processQuestionViaOrchestrator(
|
|
|
2055
2221
|
modelContext,
|
|
2056
2222
|
)
|
|
2057
2223
|
: undefined;
|
|
2224
|
+
const priorFailureHints = priorFailure
|
|
2225
|
+
? buildPriorSqlFailureHints(
|
|
2226
|
+
priorFailure.failedSql,
|
|
2227
|
+
priorFailure.failedReason,
|
|
2228
|
+
)
|
|
2229
|
+
: [];
|
|
2230
|
+
const enrichedContext =
|
|
2231
|
+
priorFailureHints.length > 0
|
|
2232
|
+
? {
|
|
2233
|
+
naturalLanguageQuery: question,
|
|
2234
|
+
...baseContext,
|
|
2235
|
+
businessContextMatch: {
|
|
2236
|
+
...baseContext?.businessContextMatch,
|
|
2237
|
+
additionalNlModelContext: [
|
|
2238
|
+
...(baseContext?.businessContextMatch
|
|
2239
|
+
?.additionalNlModelContext ?? []),
|
|
2240
|
+
...priorFailureHints,
|
|
2241
|
+
],
|
|
2242
|
+
},
|
|
2243
|
+
}
|
|
2244
|
+
: baseContext;
|
|
2058
2245
|
const orchestratorResponse = await withTimeout(
|
|
2059
2246
|
plugin.generateQueryViaOrchestrator(
|
|
2060
2247
|
{
|
|
@@ -2463,14 +2650,16 @@ export function ensureSafeLimit(
|
|
|
2463
2650
|
return `${sql.trimEnd()}\nLIMIT ${limit}`;
|
|
2464
2651
|
}
|
|
2465
2652
|
|
|
2653
|
+
/**
|
|
2654
|
+
* The one place generated SQL text is rewritten, working around engine
|
|
2655
|
+
* transpiler limits on joins. Every step no-ops on a shape it cannot fix.
|
|
2656
|
+
*/
|
|
2466
2657
|
function prepareSafeSql(sql: string, services: TDSServiceSchema[]): string {
|
|
2467
|
-
const
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
);
|
|
2473
|
-
const wrapped = wrapBareJoinAccessPoints(joined, services);
|
|
2658
|
+
const deduped = sanitizeJoinDuplicateColumns(sql, services);
|
|
2659
|
+
const ordered = sanitizeJoinOrderBy(deduped);
|
|
2660
|
+
const keyed = sanitizeJoinSameKeyColumns(ordered, services);
|
|
2661
|
+
const literals = sanitizeLiteralColumns(keyed);
|
|
2662
|
+
const wrapped = wrapBareJoinAccessPoints(literals, services);
|
|
2474
2663
|
const bounded = boundCrossAccessPointJoinDrivingSide(wrapped);
|
|
2475
2664
|
const safeSql = ensureSafeLimit(ensureDateParameters(bounded, services));
|
|
2476
2665
|
const unsupported = detectUnsupportedEnginePattern(safeSql);
|
|
@@ -2739,10 +2928,9 @@ function handleSqlGenerationFailure(
|
|
|
2739
2928
|
updateLastAssistant(setMessages, () => ({
|
|
2740
2929
|
textAnswer: orchestratorMessage,
|
|
2741
2930
|
suggestedQueries: suggestions,
|
|
2742
|
-
fallbackAction: {
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
},
|
|
2931
|
+
fallbackAction: buildOrchestratorFallbackAction({
|
|
2932
|
+
failedReason: orchestratorMessage,
|
|
2933
|
+
}),
|
|
2746
2934
|
isProcessing: false,
|
|
2747
2935
|
thinkingDuration: elapsedSeconds(startTime),
|
|
2748
2936
|
}));
|
|
@@ -2762,6 +2950,7 @@ interface QueryResultReport {
|
|
|
2762
2950
|
question: string;
|
|
2763
2951
|
services: TDSServiceSchema[];
|
|
2764
2952
|
allAccessPoints?: TDSServiceSchema[];
|
|
2953
|
+
zeroRowExplanation?: string;
|
|
2765
2954
|
}
|
|
2766
2955
|
|
|
2767
2956
|
async function reportQueryResults(
|
|
@@ -2771,7 +2960,14 @@ async function reportQueryResults(
|
|
|
2771
2960
|
startTime: number,
|
|
2772
2961
|
hasOrchestratorFallback: boolean,
|
|
2773
2962
|
): Promise<void> {
|
|
2774
|
-
const {
|
|
2963
|
+
const {
|
|
2964
|
+
currentSql,
|
|
2965
|
+
sqlResult,
|
|
2966
|
+
question,
|
|
2967
|
+
services,
|
|
2968
|
+
allAccessPoints,
|
|
2969
|
+
zeroRowExplanation,
|
|
2970
|
+
} = report;
|
|
2775
2971
|
const { config, plugin, setMessages } = context;
|
|
2776
2972
|
if (sqlResult.rows.length > 0) {
|
|
2777
2973
|
const columns = deduplicateColumns(sqlResult.columns);
|
|
@@ -2819,12 +3015,13 @@ async function reportQueryResults(
|
|
|
2819
3015
|
} else {
|
|
2820
3016
|
addThinkingStep(
|
|
2821
3017
|
setMessages,
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
currentSql,
|
|
2826
|
-
services,
|
|
3018
|
+
zeroRowExplanation === undefined
|
|
3019
|
+
? 'Query returned 0 rows after correction attempts.'
|
|
3020
|
+
: 'No overlapping join keys — the query would return 0 rows.',
|
|
2827
3021
|
);
|
|
3022
|
+
const joinExplanation =
|
|
3023
|
+
zeroRowExplanation ??
|
|
3024
|
+
buildCrossJoinZeroRowExplanation(currentSql, services);
|
|
2828
3025
|
const joinSuggestions =
|
|
2829
3026
|
joinExplanation === undefined
|
|
2830
3027
|
? []
|
|
@@ -2837,6 +3034,7 @@ async function reportQueryResults(
|
|
|
2837
3034
|
metadata,
|
|
2838
3035
|
config,
|
|
2839
3036
|
joinExplanation,
|
|
3037
|
+
services,
|
|
2840
3038
|
)
|
|
2841
3039
|
.catch((error: unknown) => {
|
|
2842
3040
|
assertErrorThrown(error);
|
|
@@ -2849,19 +3047,18 @@ async function reportQueryResults(
|
|
|
2849
3047
|
joinSuggestions.length > 0
|
|
2850
3048
|
? joinSuggestions
|
|
2851
3049
|
: (llmAnalysis?.suggestedQueries ?? []);
|
|
3050
|
+
const zeroRowReason =
|
|
3051
|
+
llmAnalysis?.summary ?? joinExplanation ?? buildZeroRowMessage(services);
|
|
2852
3052
|
const fallback = hasOrchestratorFallback
|
|
2853
3053
|
? {
|
|
2854
|
-
fallbackAction: {
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
},
|
|
3054
|
+
fallbackAction: buildOrchestratorFallbackAction({
|
|
3055
|
+
failedSql: currentSql,
|
|
3056
|
+
failedReason: zeroRowReason,
|
|
3057
|
+
}),
|
|
2858
3058
|
}
|
|
2859
3059
|
: {};
|
|
2860
3060
|
updateLastAssistant(setMessages, () => ({
|
|
2861
|
-
textAnswer:
|
|
2862
|
-
llmAnalysis?.summary ??
|
|
2863
|
-
joinExplanation ??
|
|
2864
|
-
buildZeroRowMessage(services),
|
|
3061
|
+
textAnswer: zeroRowReason,
|
|
2865
3062
|
...(suggestions.length > 0 ? { suggestedQueries: suggestions } : {}),
|
|
2866
3063
|
...fallback,
|
|
2867
3064
|
isProcessing: false,
|
|
@@ -3005,12 +3202,12 @@ export function applyMultiTurnBias(
|
|
|
3005
3202
|
// Sort so that previously-used APs come first, preserving relative order
|
|
3006
3203
|
const biased = [...services].sort((a, b) => {
|
|
3007
3204
|
const aUsed =
|
|
3008
|
-
previousAPs.has(a
|
|
3205
|
+
previousAPs.has(accessPointName(a).toLowerCase()) ||
|
|
3009
3206
|
previousAPs.has(a.title.toLowerCase())
|
|
3010
3207
|
? 1
|
|
3011
3208
|
: 0;
|
|
3012
3209
|
const bUsed =
|
|
3013
|
-
previousAPs.has(b
|
|
3210
|
+
previousAPs.has(accessPointName(b).toLowerCase()) ||
|
|
3014
3211
|
previousAPs.has(b.title.toLowerCase())
|
|
3015
3212
|
? 1
|
|
3016
3213
|
: 0;
|
|
@@ -3185,6 +3382,12 @@ export function resolveFilterLiteral(
|
|
|
3185
3382
|
|
|
3186
3383
|
type FilterValueProbe = (column: string) => Promise<string[] | undefined>;
|
|
3187
3384
|
|
|
3385
|
+
interface ColumnValueProbeOptions {
|
|
3386
|
+
rowLimit?: number;
|
|
3387
|
+
maxColumns?: number;
|
|
3388
|
+
silent?: boolean;
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3188
3391
|
// Rewrites `"col" = 'literal'` predicates in the WHERE clause whose literal
|
|
3189
3392
|
// fuzzy-matches a real probed column value, returning the updated SQL and
|
|
3190
3393
|
// whether anything changed. Every distinct predicate is corrected (a column can
|
|
@@ -3373,34 +3576,24 @@ function buildAccessPointColumnIndex(
|
|
|
3373
3576
|
return index;
|
|
3374
3577
|
}
|
|
3375
3578
|
|
|
3376
|
-
//
|
|
3377
|
-
//
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
selectedServices: TDSServiceSchema[],
|
|
3579
|
+
// Builds a memoized distinct-value prober for a query's columns: access-point
|
|
3580
|
+
// columns resolve via a relation query, service columns via SELECT DISTINCT.
|
|
3581
|
+
export function createColumnValueProbe(
|
|
3582
|
+
services: TDSServiceSchema[],
|
|
3381
3583
|
dataProductCoordinates:
|
|
3382
3584
|
| LegendAIOrchestratorDataProductCoordinates
|
|
3383
3585
|
| undefined,
|
|
3586
|
+
fromClause: string | undefined,
|
|
3587
|
+
isAccessPointProbe: boolean,
|
|
3384
3588
|
context: LegendAIOperationContext,
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
> {
|
|
3589
|
+
options?: ColumnValueProbeOptions,
|
|
3590
|
+
): FilterValueProbe {
|
|
3388
3591
|
const { plugin, config, setMessages } = context;
|
|
3389
|
-
const
|
|
3390
|
-
|
|
3391
|
-
selectedServices.some(
|
|
3392
|
-
(s) => s.sourceType === TDSServiceSourceType.ACCESS_POINT,
|
|
3393
|
-
);
|
|
3592
|
+
const rowLimit = options?.rowLimit ?? DISTINCT_PROBE_ROW_LIMIT;
|
|
3593
|
+
const maxColumns = options?.maxColumns ?? MAX_RECOVERED_FILTER_COLUMNS;
|
|
3394
3594
|
const accessPointByColumn = isAccessPointProbe
|
|
3395
|
-
? buildAccessPointColumnIndex(
|
|
3595
|
+
? buildAccessPointColumnIndex(services)
|
|
3396
3596
|
: new Map<string, TDSServiceSchema>();
|
|
3397
|
-
const fromClause = isAccessPointProbe
|
|
3398
|
-
? undefined
|
|
3399
|
-
: extractFromClause(currentSql);
|
|
3400
|
-
if (!isAccessPointProbe && fromClause === undefined) {
|
|
3401
|
-
return undefined;
|
|
3402
|
-
}
|
|
3403
|
-
|
|
3404
3597
|
const probeAccessPointColumn = async (
|
|
3405
3598
|
column: string,
|
|
3406
3599
|
): Promise<LegendAISqlExecutionResultData> => {
|
|
@@ -3409,22 +3602,19 @@ async function attemptDistinctValueRecovery(
|
|
|
3409
3602
|
if (pId === undefined) {
|
|
3410
3603
|
return { columns: [], rows: [] };
|
|
3411
3604
|
}
|
|
3412
|
-
const relationQuery = `#P{${pId}}#->select(~[${pureRelationColumnRef(column)}])->distinct()->take(${
|
|
3605
|
+
const relationQuery = `#P{${pId}}#->select(~[${pureRelationColumnRef(column)}])->distinct()->take(${rowLimit})`;
|
|
3413
3606
|
return plugin.executeLakehouseRelationQuery(
|
|
3414
3607
|
relationQuery,
|
|
3415
3608
|
guaranteeNonNullable(dataProductCoordinates),
|
|
3416
3609
|
config,
|
|
3417
3610
|
);
|
|
3418
3611
|
};
|
|
3419
|
-
|
|
3420
3612
|
const probedValues = new Map<string, string[] | undefined>();
|
|
3421
|
-
|
|
3422
|
-
column: string,
|
|
3423
|
-
): Promise<string[] | undefined> => {
|
|
3613
|
+
return async (column: string): Promise<string[] | undefined> => {
|
|
3424
3614
|
if (probedValues.has(column)) {
|
|
3425
3615
|
return probedValues.get(column);
|
|
3426
3616
|
}
|
|
3427
|
-
if (probedValues.size >=
|
|
3617
|
+
if (probedValues.size >= maxColumns) {
|
|
3428
3618
|
return undefined;
|
|
3429
3619
|
}
|
|
3430
3620
|
let values: string[] | undefined;
|
|
@@ -3432,8 +3622,8 @@ async function attemptDistinctValueRecovery(
|
|
|
3432
3622
|
const probe = isAccessPointProbe
|
|
3433
3623
|
? await probeAccessPointColumn(column)
|
|
3434
3624
|
: await executeSqlForServices(
|
|
3435
|
-
`SELECT DISTINCT "${column}" ${fromClause} LIMIT ${
|
|
3436
|
-
|
|
3625
|
+
`SELECT DISTINCT "${column}" ${fromClause} LIMIT ${rowLimit}`,
|
|
3626
|
+
services,
|
|
3437
3627
|
dataProductCoordinates,
|
|
3438
3628
|
plugin,
|
|
3439
3629
|
config,
|
|
@@ -3444,12 +3634,145 @@ async function attemptDistinctValueRecovery(
|
|
|
3444
3634
|
.map(String);
|
|
3445
3635
|
} catch (probeError) {
|
|
3446
3636
|
assertErrorThrown(probeError);
|
|
3447
|
-
|
|
3637
|
+
if (options?.silent !== true) {
|
|
3638
|
+
addThinkingStep(setMessages, `Could not probe values for "${column}"`);
|
|
3639
|
+
}
|
|
3448
3640
|
values = undefined;
|
|
3449
3641
|
}
|
|
3450
3642
|
probedValues.set(column, values);
|
|
3451
3643
|
return values;
|
|
3452
3644
|
};
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3647
|
+
// An empty inner join yields no rows, but an outer join, an aggregate, a
|
|
3648
|
+
// disjunctive predicate or a set operation can still return some.
|
|
3649
|
+
export function canSkipExecutionOnEmptyJoin(sql: string): boolean {
|
|
3650
|
+
return (
|
|
3651
|
+
!OUTER_JOIN_PATTERN.test(sql) &&
|
|
3652
|
+
!HAS_AGGREGATION_PATTERN.test(sql) &&
|
|
3653
|
+
!DISJUNCTIVE_PREDICATE_PATTERN.test(sql) &&
|
|
3654
|
+
!SET_OPERATION_PATTERN.test(sql)
|
|
3655
|
+
);
|
|
3656
|
+
}
|
|
3657
|
+
|
|
3658
|
+
/**
|
|
3659
|
+
* Reports the join key whose values do not overlap, so a two access point join
|
|
3660
|
+
* can be explained instead of run. Text keys only, because dates and numbers
|
|
3661
|
+
* serialize differently on either side and would read as disjoint when they
|
|
3662
|
+
* are not, and only for exactly two access points, because a key belonging to
|
|
3663
|
+
* another pair would otherwise be tested against the wrong relations.
|
|
3664
|
+
*/
|
|
3665
|
+
export async function probeJoinOverlap(
|
|
3666
|
+
sql: string,
|
|
3667
|
+
services: TDSServiceSchema[],
|
|
3668
|
+
joinKeys: Set<string>,
|
|
3669
|
+
dataProductCoordinates:
|
|
3670
|
+
| LegendAIOrchestratorDataProductCoordinates
|
|
3671
|
+
| undefined,
|
|
3672
|
+
context: LegendAIOperationContext,
|
|
3673
|
+
): Promise<string | undefined> {
|
|
3674
|
+
if (dataProductCoordinates === undefined) {
|
|
3675
|
+
return undefined;
|
|
3676
|
+
}
|
|
3677
|
+
const involved = resolveJoinedAccessPoints(sql, services);
|
|
3678
|
+
const apA = involved[0];
|
|
3679
|
+
const apB = involved[1];
|
|
3680
|
+
if (involved.length !== 2 || apA === undefined || apB === undefined) {
|
|
3681
|
+
return undefined;
|
|
3682
|
+
}
|
|
3683
|
+
const sharedKeys = sharedColumnNames(apA, apB)
|
|
3684
|
+
.filter((name) => joinKeys.has(name.toLowerCase()))
|
|
3685
|
+
.slice(0, MAX_JOIN_OVERLAP_PROBE_KEYS);
|
|
3686
|
+
if (sharedKeys.length === 0) {
|
|
3687
|
+
return undefined;
|
|
3688
|
+
}
|
|
3689
|
+
const columnsA = buildColumnByNameIndex(apA.columns);
|
|
3690
|
+
const columnsB = buildColumnByNameIndex(apB.columns);
|
|
3691
|
+
const probeOptions: ColumnValueProbeOptions = {
|
|
3692
|
+
rowLimit: JOIN_OVERLAP_PROBE_LIMIT,
|
|
3693
|
+
maxColumns: MAX_JOIN_OVERLAP_PROBE_KEYS,
|
|
3694
|
+
silent: true,
|
|
3695
|
+
};
|
|
3696
|
+
const probeA = createColumnValueProbe(
|
|
3697
|
+
[apA],
|
|
3698
|
+
dataProductCoordinates,
|
|
3699
|
+
undefined,
|
|
3700
|
+
true,
|
|
3701
|
+
context,
|
|
3702
|
+
probeOptions,
|
|
3703
|
+
);
|
|
3704
|
+
const probeB = createColumnValueProbe(
|
|
3705
|
+
[apB],
|
|
3706
|
+
dataProductCoordinates,
|
|
3707
|
+
undefined,
|
|
3708
|
+
true,
|
|
3709
|
+
context,
|
|
3710
|
+
probeOptions,
|
|
3711
|
+
);
|
|
3712
|
+
const fullyEnumerated = (values: string[] | undefined): values is string[] =>
|
|
3713
|
+
values !== undefined &&
|
|
3714
|
+
values.length > 0 &&
|
|
3715
|
+
values.length < JOIN_OVERLAP_CONCLUSIVE_MAX;
|
|
3716
|
+
for (const key of sharedKeys) {
|
|
3717
|
+
const columnA = columnsA.get(key.toLowerCase());
|
|
3718
|
+
const columnB = columnsB.get(key.toLowerCase());
|
|
3719
|
+
if (columnA === undefined || columnB === undefined) {
|
|
3720
|
+
continue;
|
|
3721
|
+
}
|
|
3722
|
+
const nameB = columnB.name;
|
|
3723
|
+
if (!isStringTypedColumn(columnA) || !isStringTypedColumn(columnB)) {
|
|
3724
|
+
continue;
|
|
3725
|
+
}
|
|
3726
|
+
const [valuesA, valuesB] = await Promise.all([probeA(key), probeB(nameB)]);
|
|
3727
|
+
if (!fullyEnumerated(valuesA) || !fullyEnumerated(valuesB)) {
|
|
3728
|
+
continue;
|
|
3729
|
+
}
|
|
3730
|
+
const setB = new Set(valuesB.map((v) => v.toLowerCase()));
|
|
3731
|
+
if (!valuesA.some((v) => setB.has(v.toLowerCase()))) {
|
|
3732
|
+
return buildDisjointJoinMessage(
|
|
3733
|
+
apA.title,
|
|
3734
|
+
apB.title,
|
|
3735
|
+
key,
|
|
3736
|
+
previewValues(valuesA),
|
|
3737
|
+
previewValues(valuesB),
|
|
3738
|
+
);
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
return undefined;
|
|
3742
|
+
}
|
|
3743
|
+
|
|
3744
|
+
// Recovers a zero-row query by re-probing each WHERE-filter column's real distinct
|
|
3745
|
+
// values and rewriting equality/IN literals that don't match (casing/spelling).
|
|
3746
|
+
async function attemptDistinctValueRecovery(
|
|
3747
|
+
currentSql: string,
|
|
3748
|
+
selectedServices: TDSServiceSchema[],
|
|
3749
|
+
dataProductCoordinates:
|
|
3750
|
+
| LegendAIOrchestratorDataProductCoordinates
|
|
3751
|
+
| undefined,
|
|
3752
|
+
context: LegendAIOperationContext,
|
|
3753
|
+
): Promise<
|
|
3754
|
+
{ sql: string; result: LegendAISqlExecutionResultData } | undefined
|
|
3755
|
+
> {
|
|
3756
|
+
const { plugin, config, setMessages } = context;
|
|
3757
|
+
const isAccessPointProbe =
|
|
3758
|
+
dataProductCoordinates !== undefined &&
|
|
3759
|
+
selectedServices.some(
|
|
3760
|
+
(s) => s.sourceType === TDSServiceSourceType.ACCESS_POINT,
|
|
3761
|
+
);
|
|
3762
|
+
const fromClause = isAccessPointProbe
|
|
3763
|
+
? undefined
|
|
3764
|
+
: extractFromClause(currentSql);
|
|
3765
|
+
if (!isAccessPointProbe && fromClause === undefined) {
|
|
3766
|
+
return undefined;
|
|
3767
|
+
}
|
|
3768
|
+
|
|
3769
|
+
const probeColumnValues = createColumnValueProbe(
|
|
3770
|
+
selectedServices,
|
|
3771
|
+
dataProductCoordinates,
|
|
3772
|
+
fromClause,
|
|
3773
|
+
isAccessPointProbe,
|
|
3774
|
+
context,
|
|
3775
|
+
);
|
|
3453
3776
|
|
|
3454
3777
|
await Promise.all(
|
|
3455
3778
|
collectWhereFilterColumns(currentSql)
|
|
@@ -3794,7 +4117,7 @@ async function processAccessPointQuery(
|
|
|
3794
4117
|
const selectedAPs = await selectBestServices(question, accessPoints, context);
|
|
3795
4118
|
|
|
3796
4119
|
const currentAccessPoints = new Set(
|
|
3797
|
-
selectedAPs.map((ap) => ap
|
|
4120
|
+
selectedAPs.map((ap) => accessPointName(ap)),
|
|
3798
4121
|
);
|
|
3799
4122
|
const scopedContext: LegendAIOperationContext = {
|
|
3800
4123
|
...context,
|
|
@@ -3808,6 +4131,7 @@ async function processAccessPointQuery(
|
|
|
3808
4131
|
selectedAPs,
|
|
3809
4132
|
dataProductCoordinates,
|
|
3810
4133
|
config,
|
|
4134
|
+
question,
|
|
3811
4135
|
),
|
|
3812
4136
|
VALUE_GROUNDING_TIME_BUDGET_MS,
|
|
3813
4137
|
);
|
|
@@ -3855,10 +4179,56 @@ async function processAccessPointQuery(
|
|
|
3855
4179
|
updateLastAssistant(setMessages, () => ({
|
|
3856
4180
|
sql: finalSql,
|
|
3857
4181
|
sqlGenTime: sqlGenTimeValue,
|
|
3858
|
-
isExecuting: true,
|
|
3859
4182
|
queriedAccessPoints: orderedAccessPointsFromSql(finalSql),
|
|
3860
4183
|
}));
|
|
3861
4184
|
|
|
4185
|
+
const joinKeys = canSkipExecutionOnEmptyJoin(finalSql)
|
|
4186
|
+
? extractJoinKeyColumns(finalSql)
|
|
4187
|
+
: new Set<string>();
|
|
4188
|
+
let disjointJoin =
|
|
4189
|
+
joinKeys.size > 0
|
|
4190
|
+
? detectDisjointJoinUniverses(finalSql, selectedAPs, {
|
|
4191
|
+
requireComplete: true,
|
|
4192
|
+
restrictToColumns: joinKeys,
|
|
4193
|
+
})
|
|
4194
|
+
: undefined;
|
|
4195
|
+
if (disjointJoin === undefined && joinKeys.size > 0) {
|
|
4196
|
+
addThinkingStep(setMessages, 'Probing join-key overlap...');
|
|
4197
|
+
disjointJoin = await withTimeout(
|
|
4198
|
+
probeJoinOverlap(
|
|
4199
|
+
finalSql,
|
|
4200
|
+
selectedAPs,
|
|
4201
|
+
joinKeys,
|
|
4202
|
+
dataProductCoordinates,
|
|
4203
|
+
scopedContext,
|
|
4204
|
+
),
|
|
4205
|
+
JOIN_OVERLAP_PROBE_TIMEOUT_MS,
|
|
4206
|
+
);
|
|
4207
|
+
}
|
|
4208
|
+
if (disjointJoin !== undefined) {
|
|
4209
|
+
addThinkingStep(
|
|
4210
|
+
setMessages,
|
|
4211
|
+
'Join keys do not overlap — skipping execution',
|
|
4212
|
+
);
|
|
4213
|
+
await reportQueryResults(
|
|
4214
|
+
{
|
|
4215
|
+
currentSql: finalSql,
|
|
4216
|
+
sqlResult: { columns: [], rows: [] },
|
|
4217
|
+
question,
|
|
4218
|
+
services: selectedAPs,
|
|
4219
|
+
allAccessPoints: accessPoints,
|
|
4220
|
+
zeroRowExplanation: disjointJoin,
|
|
4221
|
+
},
|
|
4222
|
+
metadata,
|
|
4223
|
+
scopedContext,
|
|
4224
|
+
startTime,
|
|
4225
|
+
false,
|
|
4226
|
+
);
|
|
4227
|
+
return;
|
|
4228
|
+
}
|
|
4229
|
+
|
|
4230
|
+
updateLastAssistant(setMessages, () => ({ isExecuting: true }));
|
|
4231
|
+
|
|
3862
4232
|
const execStartTime = Date.now();
|
|
3863
4233
|
try {
|
|
3864
4234
|
let execSql = finalSql;
|
|
@@ -3990,16 +4360,22 @@ async function processDataQuery(
|
|
|
3990
4360
|
const missingParams = detectMissingServiceParams(judgedSql, selectedServices);
|
|
3991
4361
|
if (missingParams.length > 0) {
|
|
3992
4362
|
const sqlGenTimeValue = elapsedSeconds(startTime, 2);
|
|
4363
|
+
const missingParamsReason = `Missing required parameter${missingParams.length > 1 ? 's' : ''}: ${missingParams.map((p) => p.name).join(', ')}`;
|
|
3993
4364
|
completeThinkingSteps(setMessages);
|
|
3994
|
-
addThinkingStep(
|
|
3995
|
-
setMessages,
|
|
3996
|
-
`Missing required parameter${missingParams.length > 1 ? 's' : ''}: ${missingParams.map((p) => p.name).join(', ')}`,
|
|
3997
|
-
);
|
|
4365
|
+
addThinkingStep(setMessages, missingParamsReason);
|
|
3998
4366
|
completeThinkingSteps(setMessages);
|
|
3999
4367
|
updateLastAssistant(setMessages, () => ({
|
|
4000
4368
|
sql: judgedSql,
|
|
4001
4369
|
sqlGenTime: sqlGenTimeValue,
|
|
4002
4370
|
textAnswer: buildMissingParamsWarning(missingParams),
|
|
4371
|
+
...(hasOrchestratorFallback
|
|
4372
|
+
? {
|
|
4373
|
+
fallbackAction: buildOrchestratorFallbackAction({
|
|
4374
|
+
failedSql: judgedSql,
|
|
4375
|
+
failedReason: missingParamsReason,
|
|
4376
|
+
}),
|
|
4377
|
+
}
|
|
4378
|
+
: {}),
|
|
4003
4379
|
isProcessing: false,
|
|
4004
4380
|
isExecuting: false,
|
|
4005
4381
|
thinkingDuration: elapsedSeconds(startTime),
|
|
@@ -4025,14 +4401,15 @@ async function processDataQuery(
|
|
|
4025
4401
|
);
|
|
4026
4402
|
|
|
4027
4403
|
if (execOutcome.error) {
|
|
4028
|
-
const
|
|
4404
|
+
const execError = execOutcome.error;
|
|
4405
|
+
const execErrorType = classifyError(new Error(execError));
|
|
4029
4406
|
addThinkingStep(
|
|
4030
4407
|
setMessages,
|
|
4031
|
-
`Execution failed: ${
|
|
4408
|
+
`Execution failed: ${execError.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
4032
4409
|
);
|
|
4033
4410
|
finishWithThinkingError(
|
|
4034
4411
|
setMessages,
|
|
4035
|
-
buildExecutionErrorMessage(
|
|
4412
|
+
buildExecutionErrorMessage(execError, selectedServices),
|
|
4036
4413
|
startTime,
|
|
4037
4414
|
execErrorType === LegendAIErrorType.GENERAL
|
|
4038
4415
|
? LegendAIErrorType.EXECUTION
|
|
@@ -4043,10 +4420,10 @@ async function processDataQuery(
|
|
|
4043
4420
|
suggestedQueries: buildFallbackSuggestions(selectedServices),
|
|
4044
4421
|
...(hasOrchestratorFallback
|
|
4045
4422
|
? {
|
|
4046
|
-
fallbackAction: {
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
},
|
|
4423
|
+
fallbackAction: buildOrchestratorFallbackAction({
|
|
4424
|
+
failedSql: execOutcome.sql,
|
|
4425
|
+
failedReason: execError,
|
|
4426
|
+
}),
|
|
4050
4427
|
}
|
|
4051
4428
|
: {}),
|
|
4052
4429
|
}));
|
|
@@ -4217,10 +4594,7 @@ export async function processQuestion(
|
|
|
4217
4594
|
);
|
|
4218
4595
|
if (config.orchestratorUrl && dataProductCoordinates) {
|
|
4219
4596
|
updateLastAssistant(setMessages, () => ({
|
|
4220
|
-
fallbackAction:
|
|
4221
|
-
label: ORCHESTRATOR_FALLBACK_LABEL,
|
|
4222
|
-
actionId: LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
|
|
4223
|
-
},
|
|
4597
|
+
fallbackAction: buildOrchestratorFallbackAction(),
|
|
4224
4598
|
}));
|
|
4225
4599
|
}
|
|
4226
4600
|
return;
|
|
@@ -4415,10 +4789,7 @@ export async function processQuestionWithIntent(
|
|
|
4415
4789
|
getMetadataEnrichment(),
|
|
4416
4790
|
);
|
|
4417
4791
|
updateLastAssistant(setMessages, () => ({
|
|
4418
|
-
fallbackAction:
|
|
4419
|
-
label: ORCHESTRATOR_FALLBACK_LABEL,
|
|
4420
|
-
actionId: LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
|
|
4421
|
-
},
|
|
4792
|
+
fallbackAction: buildOrchestratorFallbackAction(),
|
|
4422
4793
|
}));
|
|
4423
4794
|
return;
|
|
4424
4795
|
}
|