@finos/legend-lego 2.0.196 → 2.0.198
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/code-editor/CodeEditor.d.ts.map +1 -1
- package/lib/code-editor/CodeEditor.js +14 -1
- package/lib/code-editor/CodeEditor.js.map +1 -1
- package/lib/index.css +2 -2
- package/lib/index.css.map +1 -1
- package/lib/legend-ai/LegendAIDocEnrichment.d.ts +60 -0
- package/lib/legend-ai/LegendAIDocEnrichment.d.ts.map +1 -0
- package/lib/legend-ai/LegendAIDocEnrichment.js +429 -0
- package/lib/legend-ai/LegendAIDocEnrichment.js.map +1 -0
- package/lib/legend-ai/LegendAITypes.d.ts +127 -1
- package/lib/legend-ai/LegendAITypes.d.ts.map +1 -1
- package/lib/legend-ai/LegendAITypes.js +111 -2
- package/lib/legend-ai/LegendAITypes.js.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts +14 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js.map +1 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.d.ts +2 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.d.ts.map +1 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.js +37 -2
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.js.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisPanel.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisPanel.js +11 -12
- package/lib/legend-ai/components/LegendAIAnalysisPanel.js.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts +7 -0
- package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisUtils.js +106 -41
- package/lib/legend-ai/components/LegendAIAnalysisUtils.js.map +1 -1
- package/lib/legend-ai/components/LegendAIChat.d.ts +1 -5
- package/lib/legend-ai/components/LegendAIChat.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIChat.js +168 -109
- package/lib/legend-ai/components/LegendAIChat.js.map +1 -1
- package/lib/legend-ai/components/LegendAIChatHelpers.d.ts +21 -0
- package/lib/legend-ai/components/LegendAIChatHelpers.d.ts.map +1 -0
- package/lib/legend-ai/components/LegendAIChatHelpers.js +85 -0
- package/lib/legend-ai/components/LegendAIChatHelpers.js.map +1 -0
- package/lib/legend-ai/components/LegendAIChatInput.d.ts +21 -0
- package/lib/legend-ai/components/LegendAIChatInput.d.ts.map +1 -0
- package/lib/legend-ai/components/LegendAIChatInput.js +78 -0
- package/lib/legend-ai/components/LegendAIChatInput.js.map +1 -0
- package/lib/legend-ai/components/LegendAIScopeSelector.d.ts +25 -0
- package/lib/legend-ai/components/LegendAIScopeSelector.d.ts.map +1 -0
- package/lib/legend-ai/components/LegendAIScopeSelector.js +85 -0
- package/lib/legend-ai/components/LegendAIScopeSelector.js.map +1 -0
- package/lib/legend-ai/index.d.ts +8 -3
- package/lib/legend-ai/index.d.ts.map +1 -1
- package/lib/legend-ai/index.js +8 -3
- package/lib/legend-ai/index.js.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts +105 -0
- package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts.map +1 -0
- package/lib/legend-ai/stores/LegendAIChatProcessors.js +1482 -0
- package/lib/legend-ai/stores/LegendAIChatProcessors.js.map +1 -0
- package/lib/legend-ai/stores/LegendAIChatState.d.ts +2 -35
- package/lib/legend-ai/stores/LegendAIChatState.d.ts.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatState.js +114 -949
- package/lib/legend-ai/stores/LegendAIChatState.js.map +1 -1
- package/package.json +5 -5
- package/src/code-editor/CodeEditor.tsx +19 -0
- package/src/legend-ai/LegendAIDocEnrichment.ts +572 -0
- package/src/legend-ai/LegendAITypes.ts +213 -5
- package/src/legend-ai/LegendAI_LegendApplicationPlugin_Extension.ts +25 -0
- package/src/legend-ai/__test-utils__/LegendAITestUtils.ts +55 -1
- package/src/legend-ai/components/LegendAIAnalysisPanel.tsx +14 -34
- package/src/legend-ai/components/LegendAIAnalysisUtils.ts +157 -47
- package/src/legend-ai/components/LegendAIChat.tsx +389 -206
- package/src/legend-ai/components/LegendAIChatHelpers.ts +117 -0
- package/src/legend-ai/components/LegendAIChatInput.tsx +209 -0
- package/src/legend-ai/components/LegendAIScopeSelector.tsx +199 -0
- package/src/legend-ai/index.ts +31 -4
- package/src/legend-ai/stores/LegendAIChatProcessors.ts +2563 -0
- package/src/legend-ai/stores/LegendAIChatState.ts +161 -1697
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,2563 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026-present, Goldman Sachs
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type React from 'react';
|
|
18
|
+
import { assertErrorThrown, uuid } from '@finos/legend-shared';
|
|
19
|
+
import {
|
|
20
|
+
type TDSServiceSchema,
|
|
21
|
+
type LegendAIConfig,
|
|
22
|
+
type LegendAIAssistantMessage,
|
|
23
|
+
type LegendAIUserMessage,
|
|
24
|
+
type LegendAIMessage,
|
|
25
|
+
type LegendAIConversationTurn,
|
|
26
|
+
type LegendAIProductMetadata,
|
|
27
|
+
classifyQuestionIntentFast,
|
|
28
|
+
LegendAIQuestionIntent,
|
|
29
|
+
LegendAIThinkingStepStatus,
|
|
30
|
+
LegendAIMessageRole,
|
|
31
|
+
LegendAIErrorType,
|
|
32
|
+
LegendAIServiceError,
|
|
33
|
+
TDSServiceSourceType,
|
|
34
|
+
buildColumnDefsFromNames,
|
|
35
|
+
LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
|
|
36
|
+
getTodayISO,
|
|
37
|
+
} from '../LegendAITypes.js';
|
|
38
|
+
import {
|
|
39
|
+
type LegendAI_LegendApplicationPlugin_Extension,
|
|
40
|
+
type LegendAIOrchestratorDataProductCoordinates,
|
|
41
|
+
type LegendAISqlExecutionResultData,
|
|
42
|
+
type LegendAIResolvedEntities,
|
|
43
|
+
LegendAIJudgeVerdict,
|
|
44
|
+
} from '../LegendAI_LegendApplicationPlugin_Extension.js';
|
|
45
|
+
import {
|
|
46
|
+
type QueryExplicitExecutionContextInfo,
|
|
47
|
+
extractElementNameFromPath,
|
|
48
|
+
} from '@finos/legend-graph';
|
|
49
|
+
|
|
50
|
+
const MAX_ERROR_MESSAGE_LENGTH = 500;
|
|
51
|
+
const MAX_THINKING_ERROR_PREVIEW_LENGTH = 200;
|
|
52
|
+
const DEFAULT_MAX_JUDGE_ATTEMPTS = 5;
|
|
53
|
+
const DEFAULT_MAX_EXECUTION_RETRIES = 3;
|
|
54
|
+
const ANALYSIS_TIMEOUT_MS = 15_000;
|
|
55
|
+
const ANALYSIS_PREVIEW_ROW_LIMIT = 3;
|
|
56
|
+
const ANALYSIS_PREVIEW_VALUE_LIMIT = 40;
|
|
57
|
+
const MAX_NON_SQL_PASS_ATTEMPTS = 2;
|
|
58
|
+
const ALIAS_DOT_COL_PATTERN = /\b(?<tbl>[a-z]\w*)\s*\.\s*"(?<col>[^"]+)"/gi;
|
|
59
|
+
const JOIN_PATTERN = /\bJOIN\b/i;
|
|
60
|
+
const ORDER_BY_SPLIT = /\bORDER\s+BY\b/i;
|
|
61
|
+
const UNION_ALL_PATTERN = /\bUNION\s+ALL\b/i;
|
|
62
|
+
const LITERAL_COL_PATTERN = /,\s*'[^']*'\s+AS\s+(?:"[^"]+"|[a-z]\w*)/gi;
|
|
63
|
+
const DEFAULT_SAFETY_LIMIT = 1000;
|
|
64
|
+
const HAS_LIMIT_PATTERN = /\bLIMIT\s+\d+/i;
|
|
65
|
+
const HAS_AGGREGATION_PATTERN =
|
|
66
|
+
/\bGROUP\s+BY\b|\bCOUNT\s*\(|\bSUM\s*\(|\bAVG\s*\(|\bMIN\s*\(|\bMAX\s*\(/i;
|
|
67
|
+
const MAX_SERVICES_FOR_LLM_SELECTION = 30;
|
|
68
|
+
const ORCHESTRATOR_FALLBACK_LABEL = 'Try Legend AI Orchestrator';
|
|
69
|
+
const SQL_GENERATION_FAILURE_WITH_ORCHESTRATOR =
|
|
70
|
+
'SQL generation could not handle this query. You can try the Legend AI Orchestrator to generate a Pure query instead.';
|
|
71
|
+
const SQL_GENERATION_FAILURE_NO_ORCHESTRATOR =
|
|
72
|
+
'SQL generation could not handle this query. Try rephrasing your question.';
|
|
73
|
+
const SERVICE_PARAM_DATE_LIKE_PATTERNS: readonly RegExp[] = [
|
|
74
|
+
/date|time|day|month|year|period|asOf|businessDate|processingDate|snapshot/i,
|
|
75
|
+
/effective|valid|settle|trade|maturity|expir|inception|close|open/i,
|
|
76
|
+
/start|end|from|until|begin|report|cutoff|valuation|pricing/i,
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
function isLikelySqlQuery(text: string): boolean {
|
|
80
|
+
const trimmed = text.trim().toLowerCase();
|
|
81
|
+
return (
|
|
82
|
+
trimmed.startsWith('select') ||
|
|
83
|
+
trimmed.startsWith('with') ||
|
|
84
|
+
trimmed.startsWith('(')
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const SUGGESTED_QUERIES_DELIMITER = '---SUGGESTED_QUERIES---';
|
|
89
|
+
|
|
90
|
+
export function elapsedSeconds(startTime: number, decimals: 1 | 2 = 1): string {
|
|
91
|
+
return ((Date.now() - startTime) / 1000).toFixed(decimals);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function withTimeout<T>(
|
|
95
|
+
promise: Promise<T>,
|
|
96
|
+
ms: number,
|
|
97
|
+
): Promise<T | undefined> {
|
|
98
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
99
|
+
return Promise.race([
|
|
100
|
+
promise.finally(() => {
|
|
101
|
+
if (timer !== undefined) {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
}
|
|
104
|
+
}),
|
|
105
|
+
new Promise<undefined>((resolve) => {
|
|
106
|
+
timer = setTimeout(() => resolve(undefined), ms);
|
|
107
|
+
}),
|
|
108
|
+
]);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function deduplicateColumns(columns: string[]): string[] {
|
|
112
|
+
const seen = new Map<string, number>();
|
|
113
|
+
return columns.map((col) => {
|
|
114
|
+
const count = seen.get(col) ?? 0;
|
|
115
|
+
seen.set(col, count + 1);
|
|
116
|
+
return count === 0 ? col : `${col}_${count + 1}`;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export type MessageSetter = React.Dispatch<
|
|
121
|
+
React.SetStateAction<LegendAIMessage[]>
|
|
122
|
+
>;
|
|
123
|
+
|
|
124
|
+
export function createMessagePair(
|
|
125
|
+
text: string,
|
|
126
|
+
): [LegendAIUserMessage, LegendAIAssistantMessage] {
|
|
127
|
+
return [
|
|
128
|
+
{ id: uuid(), role: LegendAIMessageRole.USER, text },
|
|
129
|
+
{
|
|
130
|
+
id: uuid(),
|
|
131
|
+
role: LegendAIMessageRole.ASSISTANT,
|
|
132
|
+
thinkingSteps: [],
|
|
133
|
+
sql: null,
|
|
134
|
+
textAnswer: null,
|
|
135
|
+
dataContext: null,
|
|
136
|
+
gridData: null,
|
|
137
|
+
error: null,
|
|
138
|
+
errorType: null,
|
|
139
|
+
sqlGenTime: null,
|
|
140
|
+
execTime: null,
|
|
141
|
+
thinkingDuration: null,
|
|
142
|
+
isProcessing: true,
|
|
143
|
+
isExecuting: false,
|
|
144
|
+
suggestedQueries: [],
|
|
145
|
+
fallbackAction: null,
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface LegendAIOperationContext {
|
|
151
|
+
config: LegendAIConfig;
|
|
152
|
+
plugin: LegendAI_LegendApplicationPlugin_Extension;
|
|
153
|
+
history: LegendAIConversationTurn[];
|
|
154
|
+
setMessages: MessageSetter;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
interface LegendAIOrchestratorOptionsParam {
|
|
158
|
+
dataProductCoordinates: LegendAIOrchestratorDataProductCoordinates;
|
|
159
|
+
pureExecutionContext?: QueryExplicitExecutionContextInfo;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function updateLastAssistant(
|
|
163
|
+
setMessages: MessageSetter,
|
|
164
|
+
updater: (msg: LegendAIAssistantMessage) => Partial<LegendAIAssistantMessage>,
|
|
165
|
+
): void {
|
|
166
|
+
setMessages((prev) => {
|
|
167
|
+
const newMsgs = [...prev];
|
|
168
|
+
const lastIdx = newMsgs.length - 1;
|
|
169
|
+
const last = newMsgs[lastIdx];
|
|
170
|
+
if (last?.role === LegendAIMessageRole.ASSISTANT) {
|
|
171
|
+
newMsgs[lastIdx] = { ...last, ...updater(last) };
|
|
172
|
+
}
|
|
173
|
+
return newMsgs;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function addThinkingStep(
|
|
178
|
+
setMessages: MessageSetter,
|
|
179
|
+
label: string,
|
|
180
|
+
): void {
|
|
181
|
+
updateLastAssistant(setMessages, (msg) => ({
|
|
182
|
+
thinkingSteps: [
|
|
183
|
+
...msg.thinkingSteps.map((s) =>
|
|
184
|
+
s.status === LegendAIThinkingStepStatus.ACTIVE
|
|
185
|
+
? { ...s, status: LegendAIThinkingStepStatus.DONE }
|
|
186
|
+
: s,
|
|
187
|
+
),
|
|
188
|
+
{ id: uuid(), label, status: LegendAIThinkingStepStatus.ACTIVE },
|
|
189
|
+
],
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function completeThinkingSteps(setMessages: MessageSetter): void {
|
|
194
|
+
updateLastAssistant(setMessages, (msg) => ({
|
|
195
|
+
thinkingSteps: msg.thinkingSteps.map((s) =>
|
|
196
|
+
s.status === LegendAIThinkingStepStatus.ACTIVE
|
|
197
|
+
? { ...s, status: LegendAIThinkingStepStatus.DONE }
|
|
198
|
+
: s,
|
|
199
|
+
),
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function classifyError(error: Error): LegendAIErrorType {
|
|
204
|
+
if (error instanceof LegendAIServiceError) {
|
|
205
|
+
return error.errorType;
|
|
206
|
+
}
|
|
207
|
+
return LegendAIErrorType.GENERAL;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function finishWithThinkingError(
|
|
211
|
+
setMessages: MessageSetter,
|
|
212
|
+
errorMsg: string,
|
|
213
|
+
startTime: number,
|
|
214
|
+
errorType?: LegendAIErrorType,
|
|
215
|
+
): void {
|
|
216
|
+
updateLastAssistant(setMessages, (msg) => ({
|
|
217
|
+
thinkingSteps: msg.thinkingSteps.map((s) =>
|
|
218
|
+
s.status === LegendAIThinkingStepStatus.ACTIVE
|
|
219
|
+
? { ...s, status: LegendAIThinkingStepStatus.ERROR }
|
|
220
|
+
: s,
|
|
221
|
+
),
|
|
222
|
+
error: errorMsg.slice(0, MAX_ERROR_MESSAGE_LENGTH),
|
|
223
|
+
errorType: errorType ?? null,
|
|
224
|
+
isProcessing: false,
|
|
225
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
226
|
+
}));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function buildTurnFromAssistant(
|
|
230
|
+
userText: string,
|
|
231
|
+
asstMsg: LegendAIAssistantMessage,
|
|
232
|
+
): LegendAIConversationTurn | undefined {
|
|
233
|
+
if (asstMsg.sql) {
|
|
234
|
+
const turn: LegendAIConversationTurn = {
|
|
235
|
+
question: userText,
|
|
236
|
+
sql: asstMsg.sql,
|
|
237
|
+
intent: LegendAIQuestionIntent.DATA_QUERY,
|
|
238
|
+
};
|
|
239
|
+
if (asstMsg.gridData) {
|
|
240
|
+
turn.rowCount = asstMsg.gridData.rowData.length;
|
|
241
|
+
const colNames = asstMsg.gridData.columnDefs
|
|
242
|
+
.map((c) => c.headerName ?? c.colId ?? '')
|
|
243
|
+
.filter((n) => n.length > 0);
|
|
244
|
+
if (colNames.length > 0) {
|
|
245
|
+
turn.resultSummary = `Columns: ${colNames.join(', ')}; ${turn.rowCount} row(s) returned`;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return turn;
|
|
249
|
+
}
|
|
250
|
+
if (asstMsg.textAnswer) {
|
|
251
|
+
return {
|
|
252
|
+
question: userText,
|
|
253
|
+
sql: asstMsg.textAnswer,
|
|
254
|
+
intent: LegendAIQuestionIntent.METADATA,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function buildConversationHistory(
|
|
261
|
+
messages: LegendAIMessage[],
|
|
262
|
+
): LegendAIConversationTurn[] {
|
|
263
|
+
const history: LegendAIConversationTurn[] = [];
|
|
264
|
+
let i = 0;
|
|
265
|
+
while (i < messages.length - 1) {
|
|
266
|
+
const userMsg = messages[i];
|
|
267
|
+
const asstMsg = messages[i + 1];
|
|
268
|
+
if (
|
|
269
|
+
userMsg?.role === LegendAIMessageRole.USER &&
|
|
270
|
+
asstMsg?.role === LegendAIMessageRole.ASSISTANT
|
|
271
|
+
) {
|
|
272
|
+
const turn = buildTurnFromAssistant(userMsg.text, asstMsg);
|
|
273
|
+
if (turn) {
|
|
274
|
+
history.push(turn);
|
|
275
|
+
}
|
|
276
|
+
i += 2;
|
|
277
|
+
} else {
|
|
278
|
+
i += 1;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return history;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function formatServiceParams(services: TDSServiceSchema[]): string[] {
|
|
285
|
+
return services.flatMap((s) => {
|
|
286
|
+
if (s.parameterSchemas && s.parameterSchemas.length > 0) {
|
|
287
|
+
return [
|
|
288
|
+
`${s.title}: ${s.parameterSchemas
|
|
289
|
+
.map((ps) => {
|
|
290
|
+
const parts = [ps.name];
|
|
291
|
+
if (ps.type) {
|
|
292
|
+
parts.push(`(${ps.type})`);
|
|
293
|
+
}
|
|
294
|
+
return parts.join(' ');
|
|
295
|
+
})
|
|
296
|
+
.join(', ')}`,
|
|
297
|
+
];
|
|
298
|
+
}
|
|
299
|
+
return s.parameters.length > 0
|
|
300
|
+
? [`${s.title}: ${s.parameters.join(', ')}`]
|
|
301
|
+
: [];
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function buildGenerationFailureMessage(
|
|
306
|
+
failure: string,
|
|
307
|
+
suggestion: string | undefined,
|
|
308
|
+
services: TDSServiceSchema[],
|
|
309
|
+
): string {
|
|
310
|
+
const parts = [failure];
|
|
311
|
+
if (suggestion) {
|
|
312
|
+
parts.push(`\nTry instead: "${suggestion}"`);
|
|
313
|
+
}
|
|
314
|
+
const svcNames = services.map((s) => s.title);
|
|
315
|
+
if (svcNames.length > 0) {
|
|
316
|
+
parts.push(`\nAvailable services: ${svcNames.join(', ')}`);
|
|
317
|
+
}
|
|
318
|
+
const allParams = formatServiceParams(services);
|
|
319
|
+
if (allParams.length > 0) {
|
|
320
|
+
parts.push(`\nService parameters: ${allParams.join('; ')}`);
|
|
321
|
+
}
|
|
322
|
+
return parts.join('');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function buildFallbackSuggestions(services: TDSServiceSchema[]): string[] {
|
|
326
|
+
return services.slice(0, 3).map((svc) => `Show 10 records from ${svc.title}`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function appendFallbackSuggestions(
|
|
330
|
+
setMessages: MessageSetter,
|
|
331
|
+
services: TDSServiceSchema[],
|
|
332
|
+
): void {
|
|
333
|
+
const suggestions = buildFallbackSuggestions(services);
|
|
334
|
+
if (suggestions.length > 0) {
|
|
335
|
+
updateLastAssistant(setMessages, () => ({
|
|
336
|
+
suggestedQueries: suggestions,
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function buildExecutionErrorMessage(
|
|
342
|
+
errStr: string,
|
|
343
|
+
services: TDSServiceSchema[],
|
|
344
|
+
): string {
|
|
345
|
+
const errParts: string[] = [];
|
|
346
|
+
const errLower = errStr.toLowerCase();
|
|
347
|
+
|
|
348
|
+
const missingParamMatch =
|
|
349
|
+
/missing required parameter values?\s*\[(?<params>[^\]]+)\]/i.exec(errStr);
|
|
350
|
+
if (missingParamMatch) {
|
|
351
|
+
const paramNames = missingParamMatch.groups?.params ?? '';
|
|
352
|
+
const paramList = paramNames.split(',').map((p) => p.trim());
|
|
353
|
+
const hint = paramList
|
|
354
|
+
.map(
|
|
355
|
+
(p) =>
|
|
356
|
+
`a specific ${p.replaceAll(/(?<lower>[a-z])(?<upper>[A-Z])/g, '$<lower> $<upper>').toLowerCase()}`,
|
|
357
|
+
)
|
|
358
|
+
.join(' and ');
|
|
359
|
+
errParts.push(
|
|
360
|
+
`This service requires a value for: ${paramNames}`,
|
|
361
|
+
`\nTry rephrasing your question to include ${hint}.`,
|
|
362
|
+
);
|
|
363
|
+
const svcParams = formatServiceParams(services);
|
|
364
|
+
if (svcParams.length > 0) {
|
|
365
|
+
errParts.push(`\nService parameters:\n${svcParams.join('\n')}`);
|
|
366
|
+
}
|
|
367
|
+
return errParts.join('');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
errParts.push(errStr.slice(0, MAX_ERROR_MESSAGE_LENGTH));
|
|
371
|
+
if (
|
|
372
|
+
errLower.includes('column') &&
|
|
373
|
+
(errLower.includes('not found') ||
|
|
374
|
+
errLower.includes('does not exist') ||
|
|
375
|
+
errLower.includes('unknown'))
|
|
376
|
+
) {
|
|
377
|
+
const svcCols = services.map(
|
|
378
|
+
(s) => `${s.title}: ${s.columns.map((c) => c.name).join(', ')}`,
|
|
379
|
+
);
|
|
380
|
+
errParts.push(`\nAvailable columns:\n${svcCols.join('\n')}`);
|
|
381
|
+
}
|
|
382
|
+
if (errLower.includes('parameter') || errLower.includes('argument')) {
|
|
383
|
+
const svcParams = formatServiceParams(services);
|
|
384
|
+
if (svcParams.length > 0) {
|
|
385
|
+
errParts.push(`\nRequired parameters:\n${svcParams.join('\n')}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return errParts.join('');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function parseSuggestedQueries(rawAnswer: string): {
|
|
392
|
+
answer: string;
|
|
393
|
+
suggestedQueries: string[];
|
|
394
|
+
} {
|
|
395
|
+
const delimIndex = rawAnswer.indexOf(SUGGESTED_QUERIES_DELIMITER);
|
|
396
|
+
if (delimIndex === -1) {
|
|
397
|
+
return { answer: rawAnswer.trim(), suggestedQueries: [] };
|
|
398
|
+
}
|
|
399
|
+
const answer = rawAnswer.slice(0, delimIndex).trim();
|
|
400
|
+
const suggestionsBlock = rawAnswer.slice(
|
|
401
|
+
delimIndex + SUGGESTED_QUERIES_DELIMITER.length,
|
|
402
|
+
);
|
|
403
|
+
const suggestedQueries = suggestionsBlock
|
|
404
|
+
.split('\n')
|
|
405
|
+
.map((line) => line.replace(/^\d+[.)]\s*/, '').trim())
|
|
406
|
+
.filter((line) => line.length > 0)
|
|
407
|
+
.slice(0, 3);
|
|
408
|
+
return { answer, suggestedQueries };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function tokenizeQuestionForCoverage(question: string): string[] {
|
|
412
|
+
return question
|
|
413
|
+
.toLowerCase()
|
|
414
|
+
.split(/[^a-z0-9]+/)
|
|
415
|
+
.filter((token) => token.length >= 4);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function buildQuestionCoverageNote(
|
|
419
|
+
question: string,
|
|
420
|
+
columns: string[],
|
|
421
|
+
): string {
|
|
422
|
+
const questionTokens = tokenizeQuestionForCoverage(question);
|
|
423
|
+
if (questionTokens.length === 0 || columns.length === 0) {
|
|
424
|
+
return '';
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const normalizedColumns = columns.map((c) => c.toLowerCase());
|
|
428
|
+
const matchedTokenCount = questionTokens.filter((token) =>
|
|
429
|
+
normalizedColumns.some((column) => column.includes(token)),
|
|
430
|
+
).length;
|
|
431
|
+
|
|
432
|
+
if (matchedTokenCount > 0) {
|
|
433
|
+
return '';
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return 'The retrieved columns do not clearly map to the key terms in your question. Consider refining the question with specific fields, entities, or filters.';
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function formatPreviewValue(value: unknown): string {
|
|
440
|
+
if (value === null || value === undefined) {
|
|
441
|
+
return 'null';
|
|
442
|
+
}
|
|
443
|
+
const raw = typeof value === 'string' ? value : JSON.stringify(value);
|
|
444
|
+
if (raw.length <= ANALYSIS_PREVIEW_VALUE_LIMIT) {
|
|
445
|
+
return raw;
|
|
446
|
+
}
|
|
447
|
+
return `${raw.slice(0, ANALYSIS_PREVIEW_VALUE_LIMIT)}...`;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function buildDeterministicResultSummary(
|
|
451
|
+
question: string,
|
|
452
|
+
_query: string,
|
|
453
|
+
columns: string[],
|
|
454
|
+
rows: unknown[],
|
|
455
|
+
): string {
|
|
456
|
+
const selectedColumns = columns.length === 0 ? 'none' : columns.join(', ');
|
|
457
|
+
const rowCount = rows.length;
|
|
458
|
+
const previewRows = rows
|
|
459
|
+
.slice(0, ANALYSIS_PREVIEW_ROW_LIMIT)
|
|
460
|
+
.map((row, index) => {
|
|
461
|
+
if (row && typeof row === 'object' && !Array.isArray(row)) {
|
|
462
|
+
const entries = Object.entries(row as Record<string, unknown>).slice(
|
|
463
|
+
0,
|
|
464
|
+
4,
|
|
465
|
+
);
|
|
466
|
+
const formattedEntries = entries.map(
|
|
467
|
+
([key, value]) => `${key}: ${formatPreviewValue(value)}`,
|
|
468
|
+
);
|
|
469
|
+
return `${index + 1}. ${formattedEntries.join(', ')}`;
|
|
470
|
+
}
|
|
471
|
+
return `${index + 1}. ${formatPreviewValue(row)}`;
|
|
472
|
+
})
|
|
473
|
+
.join('\n');
|
|
474
|
+
|
|
475
|
+
const coverageNote = buildQuestionCoverageNote(question, columns);
|
|
476
|
+
const parts = [
|
|
477
|
+
`I retrieved ${rowCount} row${rowCount === 1 ? '' : 's'} for your question using this query.`,
|
|
478
|
+
`Columns returned: ${selectedColumns}.`,
|
|
479
|
+
`Sample rows:\n${previewRows || 'No sample rows available.'}`,
|
|
480
|
+
];
|
|
481
|
+
if (coverageNote) {
|
|
482
|
+
parts.push(coverageNote);
|
|
483
|
+
}
|
|
484
|
+
return parts.join('\n\n');
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export async function buildMetadataOverview(
|
|
488
|
+
question: string,
|
|
489
|
+
metadata: LegendAIProductMetadata,
|
|
490
|
+
context: LegendAIOperationContext,
|
|
491
|
+
): Promise<string> {
|
|
492
|
+
const { plugin, config, history } = context;
|
|
493
|
+
const metadataPromptText = plugin.buildMetadataPrompt(
|
|
494
|
+
question,
|
|
495
|
+
metadata,
|
|
496
|
+
history,
|
|
497
|
+
);
|
|
498
|
+
const rawAnswer = await plugin.callLLM(metadataPromptText, config);
|
|
499
|
+
return parseSuggestedQueries(rawAnswer).answer;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function mergeMetadataAndQueryAnalysis(
|
|
503
|
+
metadataOverview: string,
|
|
504
|
+
queryAnalysis: string | null,
|
|
505
|
+
): string {
|
|
506
|
+
const metadata = metadataOverview.trim();
|
|
507
|
+
const analysis = queryAnalysis?.trim();
|
|
508
|
+
if (!analysis) {
|
|
509
|
+
return `### Metadata context\n${metadata}`;
|
|
510
|
+
}
|
|
511
|
+
return `### Metadata context\n${metadata}\n\n### Query analysis\n${analysis}`;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export function attachMetadataOverview(
|
|
515
|
+
setMessages: MessageSetter,
|
|
516
|
+
metadataOverview: string,
|
|
517
|
+
): void {
|
|
518
|
+
const normalized = metadataOverview.trim();
|
|
519
|
+
if (!normalized) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
updateLastAssistant(setMessages, (msg) => {
|
|
523
|
+
const existing = msg.textAnswer?.trim() ?? null;
|
|
524
|
+
if (existing?.includes('### Metadata context')) {
|
|
525
|
+
return {};
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
textAnswer: mergeMetadataAndQueryAnalysis(normalized, existing),
|
|
529
|
+
};
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
export async function handleMetadataQuestion(
|
|
534
|
+
question: string,
|
|
535
|
+
metadata: LegendAIProductMetadata,
|
|
536
|
+
context: LegendAIOperationContext,
|
|
537
|
+
startTime: number,
|
|
538
|
+
hasQueryableServices?: boolean,
|
|
539
|
+
): Promise<void> {
|
|
540
|
+
const { config, plugin, history, setMessages } = context;
|
|
541
|
+
addThinkingStep(setMessages, 'Answering from product metadata...');
|
|
542
|
+
const metadataPromptText = plugin.buildMetadataPrompt(
|
|
543
|
+
question,
|
|
544
|
+
metadata,
|
|
545
|
+
history,
|
|
546
|
+
);
|
|
547
|
+
const rawAnswer = await plugin.callLLM(metadataPromptText, config);
|
|
548
|
+
const { answer, suggestedQueries: parsedSuggestions } =
|
|
549
|
+
parseSuggestedQueries(rawAnswer);
|
|
550
|
+
const suggestedQueries =
|
|
551
|
+
hasQueryableServices === false && !config.orchestratorUrl
|
|
552
|
+
? []
|
|
553
|
+
: parsedSuggestions;
|
|
554
|
+
completeThinkingSteps(setMessages);
|
|
555
|
+
updateLastAssistant(setMessages, () => ({
|
|
556
|
+
textAnswer: answer,
|
|
557
|
+
suggestedQueries,
|
|
558
|
+
isProcessing: false,
|
|
559
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
560
|
+
}));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function handleNonSqlPass(
|
|
564
|
+
setMessages: MessageSetter,
|
|
565
|
+
nonSqlPassAttempts: number,
|
|
566
|
+
attempt: number,
|
|
567
|
+
maxAttempts: number,
|
|
568
|
+
): 'continue' | 'abort' {
|
|
569
|
+
addThinkingStep(
|
|
570
|
+
setMessages,
|
|
571
|
+
'Judge approved a non-SQL draft, requesting query correction...',
|
|
572
|
+
);
|
|
573
|
+
if (
|
|
574
|
+
attempt === maxAttempts ||
|
|
575
|
+
nonSqlPassAttempts >= MAX_NON_SQL_PASS_ATTEMPTS
|
|
576
|
+
) {
|
|
577
|
+
addThinkingStep(
|
|
578
|
+
setMessages,
|
|
579
|
+
'Max verification attempts reached without a valid SQL query',
|
|
580
|
+
);
|
|
581
|
+
return 'abort';
|
|
582
|
+
}
|
|
583
|
+
return 'continue';
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function finalizeJudgeAttempt(
|
|
587
|
+
currentSql: string,
|
|
588
|
+
previousSql: string,
|
|
589
|
+
attempt: number,
|
|
590
|
+
maxAttempts: number,
|
|
591
|
+
setMessages: MessageSetter,
|
|
592
|
+
): string | null | undefined {
|
|
593
|
+
if (currentSql !== previousSql && attempt < maxAttempts) {
|
|
594
|
+
return undefined;
|
|
595
|
+
}
|
|
596
|
+
addThinkingStep(
|
|
597
|
+
setMessages,
|
|
598
|
+
isLikelySqlQuery(currentSql)
|
|
599
|
+
? 'Max verification attempts reached, using best query'
|
|
600
|
+
: 'Max verification attempts reached without a valid SQL query',
|
|
601
|
+
);
|
|
602
|
+
return isLikelySqlQuery(currentSql) ? currentSql : null;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async function runJudgeLoop(
|
|
606
|
+
generatedSql: string,
|
|
607
|
+
buildJudgePromptFn: (sql: string) => string,
|
|
608
|
+
context: LegendAIOperationContext,
|
|
609
|
+
): Promise<string | null> {
|
|
610
|
+
const { config, plugin, setMessages } = context;
|
|
611
|
+
const maxAttempts = config.maxJudgeAttempts ?? DEFAULT_MAX_JUDGE_ATTEMPTS;
|
|
612
|
+
let currentSql = generatedSql;
|
|
613
|
+
let nonSqlPassAttempts = 0;
|
|
614
|
+
|
|
615
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
616
|
+
addThinkingStep(
|
|
617
|
+
setMessages,
|
|
618
|
+
`Verifying query correctness (${attempt}/${maxAttempts})...`,
|
|
619
|
+
);
|
|
620
|
+
const judgePrompt = buildJudgePromptFn(currentSql);
|
|
621
|
+
const judgeAnswer = await plugin.callLLM(judgePrompt, config);
|
|
622
|
+
const judgeResult = plugin.extractJudgeResult(judgeAnswer);
|
|
623
|
+
|
|
624
|
+
if (judgeResult.verdict === LegendAIJudgeVerdict.PASS) {
|
|
625
|
+
if (!isLikelySqlQuery(currentSql)) {
|
|
626
|
+
nonSqlPassAttempts++;
|
|
627
|
+
const action = handleNonSqlPass(
|
|
628
|
+
setMessages,
|
|
629
|
+
nonSqlPassAttempts,
|
|
630
|
+
attempt,
|
|
631
|
+
maxAttempts,
|
|
632
|
+
);
|
|
633
|
+
if (action === 'abort') {
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
completeThinkingSteps(setMessages);
|
|
639
|
+
return currentSql;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const previousSql = currentSql;
|
|
643
|
+
const correctedSql = judgeResult.correctedSql?.trim();
|
|
644
|
+
if (correctedSql !== undefined && isLikelySqlQuery(correctedSql)) {
|
|
645
|
+
addThinkingStep(setMessages, `Query corrected (attempt ${attempt})`);
|
|
646
|
+
currentSql = correctedSql;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const result = finalizeJudgeAttempt(
|
|
650
|
+
currentSql,
|
|
651
|
+
previousSql,
|
|
652
|
+
attempt,
|
|
653
|
+
maxAttempts,
|
|
654
|
+
setMessages,
|
|
655
|
+
);
|
|
656
|
+
if (result !== undefined) {
|
|
657
|
+
return result;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
export async function generateAndJudgeSql(
|
|
665
|
+
question: string,
|
|
666
|
+
services: TDSServiceSchema[],
|
|
667
|
+
coordinates: string,
|
|
668
|
+
context: LegendAIOperationContext,
|
|
669
|
+
startTime: number,
|
|
670
|
+
metadata?: LegendAIProductMetadata,
|
|
671
|
+
): Promise<string | null> {
|
|
672
|
+
const { plugin, config, history, setMessages } = context;
|
|
673
|
+
addThinkingStep(setMessages, 'Building context from service schemas...');
|
|
674
|
+
const prompt = plugin.buildGeneratorPrompt(
|
|
675
|
+
question,
|
|
676
|
+
services,
|
|
677
|
+
coordinates,
|
|
678
|
+
history,
|
|
679
|
+
metadata,
|
|
680
|
+
);
|
|
681
|
+
addThinkingStep(setMessages, 'Generating SQL query...');
|
|
682
|
+
|
|
683
|
+
const answerText = await plugin.callLLM(prompt, config);
|
|
684
|
+
const {
|
|
685
|
+
sql: generatedSql,
|
|
686
|
+
failure,
|
|
687
|
+
suggestion,
|
|
688
|
+
} = plugin.extractSqlFromResponse(answerText);
|
|
689
|
+
|
|
690
|
+
if (failure) {
|
|
691
|
+
addThinkingStep(setMessages, `Generation failed: ${failure}`);
|
|
692
|
+
finishWithThinkingError(
|
|
693
|
+
setMessages,
|
|
694
|
+
buildGenerationFailureMessage(failure, suggestion, services),
|
|
695
|
+
startTime,
|
|
696
|
+
LegendAIErrorType.GENERATION,
|
|
697
|
+
);
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (!generatedSql) {
|
|
702
|
+
addThinkingStep(setMessages, 'Could not extract SQL from response');
|
|
703
|
+
finishWithThinkingError(
|
|
704
|
+
setMessages,
|
|
705
|
+
'Could not extract SQL from LLM response.\nTry rephrasing your question or ask about a specific service.',
|
|
706
|
+
startTime,
|
|
707
|
+
LegendAIErrorType.GENERATION,
|
|
708
|
+
);
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return runJudgeLoop(
|
|
713
|
+
generatedSql,
|
|
714
|
+
(sql) =>
|
|
715
|
+
plugin.buildJudgePrompt(sql, question, services, coordinates, history),
|
|
716
|
+
context,
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Dedicated generate-and-judge loop for access point queries.
|
|
722
|
+
* Uses AP-specific prompts that focus on `p()` syntax and omit
|
|
723
|
+
* coordinates, parameters, and service()-related rules.
|
|
724
|
+
*/
|
|
725
|
+
export async function generateAndJudgeAccessPointSql(
|
|
726
|
+
question: string,
|
|
727
|
+
accessPoints: TDSServiceSchema[],
|
|
728
|
+
context: LegendAIOperationContext,
|
|
729
|
+
startTime: number,
|
|
730
|
+
): Promise<string | null> {
|
|
731
|
+
const { plugin, config, history, setMessages } = context;
|
|
732
|
+
addThinkingStep(setMessages, 'Building context from access point schemas...');
|
|
733
|
+
const prompt = plugin.buildAccessPointGeneratorPrompt(
|
|
734
|
+
question,
|
|
735
|
+
accessPoints,
|
|
736
|
+
history,
|
|
737
|
+
);
|
|
738
|
+
addThinkingStep(setMessages, 'Generating SQL query for access points...');
|
|
739
|
+
|
|
740
|
+
const answerText = await plugin.callLLM(prompt, config);
|
|
741
|
+
const {
|
|
742
|
+
sql: generatedSql,
|
|
743
|
+
failure,
|
|
744
|
+
suggestion,
|
|
745
|
+
} = plugin.extractSqlFromResponse(answerText);
|
|
746
|
+
|
|
747
|
+
if (failure) {
|
|
748
|
+
addThinkingStep(setMessages, `Generation failed: ${failure}`);
|
|
749
|
+
finishWithThinkingError(
|
|
750
|
+
setMessages,
|
|
751
|
+
buildGenerationFailureMessage(failure, suggestion, accessPoints),
|
|
752
|
+
startTime,
|
|
753
|
+
LegendAIErrorType.GENERATION,
|
|
754
|
+
);
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (!generatedSql) {
|
|
759
|
+
addThinkingStep(setMessages, 'Could not extract SQL from response');
|
|
760
|
+
finishWithThinkingError(
|
|
761
|
+
setMessages,
|
|
762
|
+
'Could not extract SQL from LLM response.\nTry rephrasing your question or ask about a specific access point.',
|
|
763
|
+
startTime,
|
|
764
|
+
LegendAIErrorType.GENERATION,
|
|
765
|
+
);
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
return runJudgeLoop(
|
|
770
|
+
generatedSql,
|
|
771
|
+
(sql) =>
|
|
772
|
+
plugin.buildAccessPointJudgePrompt(sql, question, accessPoints, history),
|
|
773
|
+
context,
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function reportExecutionResult(
|
|
778
|
+
rawResult: LegendAISqlExecutionResultData,
|
|
779
|
+
setMessages: MessageSetter,
|
|
780
|
+
execStartTime: number,
|
|
781
|
+
startTime: number,
|
|
782
|
+
): LegendAISqlExecutionResultData {
|
|
783
|
+
const columns = deduplicateColumns(rawResult.columns);
|
|
784
|
+
const rows = rawResult.rows;
|
|
785
|
+
completeThinkingSteps(setMessages);
|
|
786
|
+
addThinkingStep(
|
|
787
|
+
setMessages,
|
|
788
|
+
`Retrieved ${rows.length} row${rows.length === 1 ? '' : 's'}`,
|
|
789
|
+
);
|
|
790
|
+
completeThinkingSteps(setMessages);
|
|
791
|
+
|
|
792
|
+
updateLastAssistant(setMessages, () => ({
|
|
793
|
+
gridData: { columnDefs: buildColumnDefsFromNames(columns), rowData: rows },
|
|
794
|
+
execTime: elapsedSeconds(execStartTime, 2),
|
|
795
|
+
isProcessing: false,
|
|
796
|
+
isExecuting: false,
|
|
797
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
798
|
+
}));
|
|
799
|
+
return { columns, rows };
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
export async function executeSqlAndReport(
|
|
803
|
+
sql: string,
|
|
804
|
+
services: TDSServiceSchema[],
|
|
805
|
+
config: LegendAIConfig,
|
|
806
|
+
plugin: LegendAI_LegendApplicationPlugin_Extension,
|
|
807
|
+
setMessages: MessageSetter,
|
|
808
|
+
startTime: number,
|
|
809
|
+
dataProductCoordinates?: LegendAIOrchestratorDataProductCoordinates,
|
|
810
|
+
): Promise<LegendAISqlExecutionResultData | undefined> {
|
|
811
|
+
const execStartTime = Date.now();
|
|
812
|
+
try {
|
|
813
|
+
const isAccessPoint = services.some(
|
|
814
|
+
(s) => s.sourceType === TDSServiceSourceType.ACCESS_POINT,
|
|
815
|
+
);
|
|
816
|
+
const rawResult =
|
|
817
|
+
isAccessPoint && dataProductCoordinates
|
|
818
|
+
? await plugin.executeLakehouseSql(sql, dataProductCoordinates, config)
|
|
819
|
+
: await plugin.executeSql(sql, config);
|
|
820
|
+
return reportExecutionResult(
|
|
821
|
+
rawResult,
|
|
822
|
+
setMessages,
|
|
823
|
+
execStartTime,
|
|
824
|
+
startTime,
|
|
825
|
+
);
|
|
826
|
+
} catch (executeError) {
|
|
827
|
+
assertErrorThrown(executeError);
|
|
828
|
+
const execErrorType = classifyError(executeError);
|
|
829
|
+
addThinkingStep(
|
|
830
|
+
setMessages,
|
|
831
|
+
`Execution failed: ${executeError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
832
|
+
);
|
|
833
|
+
finishWithThinkingError(
|
|
834
|
+
setMessages,
|
|
835
|
+
buildExecutionErrorMessage(executeError.message, services),
|
|
836
|
+
startTime,
|
|
837
|
+
execErrorType === LegendAIErrorType.GENERAL
|
|
838
|
+
? LegendAIErrorType.EXECUTION
|
|
839
|
+
: execErrorType,
|
|
840
|
+
);
|
|
841
|
+
updateLastAssistant(setMessages, () => ({
|
|
842
|
+
execTime: elapsedSeconds(execStartTime, 2),
|
|
843
|
+
isExecuting: false,
|
|
844
|
+
}));
|
|
845
|
+
return undefined;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
export async function executePureQueryAndReport(
|
|
850
|
+
pureQuery: string,
|
|
851
|
+
pureExecutionContext: QueryExplicitExecutionContextInfo,
|
|
852
|
+
dataProductCoordinates: LegendAIOrchestratorDataProductCoordinates,
|
|
853
|
+
config: LegendAIConfig,
|
|
854
|
+
plugin: LegendAI_LegendApplicationPlugin_Extension,
|
|
855
|
+
setMessages: MessageSetter,
|
|
856
|
+
startTime: number,
|
|
857
|
+
): Promise<LegendAISqlExecutionResultData> {
|
|
858
|
+
const execStartTime = Date.now();
|
|
859
|
+
try {
|
|
860
|
+
addThinkingStep(setMessages, 'Executing Pure query...');
|
|
861
|
+
const rawResult = await plugin.executePureQuery(
|
|
862
|
+
pureQuery,
|
|
863
|
+
pureExecutionContext,
|
|
864
|
+
dataProductCoordinates,
|
|
865
|
+
config,
|
|
866
|
+
);
|
|
867
|
+
return reportExecutionResult(
|
|
868
|
+
rawResult,
|
|
869
|
+
setMessages,
|
|
870
|
+
execStartTime,
|
|
871
|
+
startTime,
|
|
872
|
+
);
|
|
873
|
+
} catch (executeError) {
|
|
874
|
+
assertErrorThrown(executeError);
|
|
875
|
+
const execErrorType = classifyError(executeError);
|
|
876
|
+
addThinkingStep(
|
|
877
|
+
setMessages,
|
|
878
|
+
`Execution failed: ${executeError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
879
|
+
);
|
|
880
|
+
completeThinkingSteps(setMessages);
|
|
881
|
+
updateLastAssistant(setMessages, () => ({
|
|
882
|
+
execTime: elapsedSeconds(execStartTime, 2),
|
|
883
|
+
isExecuting: false,
|
|
884
|
+
isProcessing: false,
|
|
885
|
+
error: `Execution failed: ${executeError.message.slice(0, MAX_ERROR_MESSAGE_LENGTH)}`,
|
|
886
|
+
errorType:
|
|
887
|
+
execErrorType === LegendAIErrorType.GENERAL
|
|
888
|
+
? LegendAIErrorType.EXECUTION
|
|
889
|
+
: execErrorType,
|
|
890
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
891
|
+
}));
|
|
892
|
+
return { columns: [], rows: [] };
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
export async function analyzeOrchestratorResults(
|
|
897
|
+
question: string,
|
|
898
|
+
query: string,
|
|
899
|
+
execResult: LegendAISqlExecutionResultData,
|
|
900
|
+
metadata: LegendAIProductMetadata,
|
|
901
|
+
context: LegendAIOperationContext,
|
|
902
|
+
startTime: number,
|
|
903
|
+
): Promise<void> {
|
|
904
|
+
const { config, plugin, setMessages } = context;
|
|
905
|
+
addThinkingStep(setMessages, 'Analyzing results...');
|
|
906
|
+
updateLastAssistant(setMessages, () => ({
|
|
907
|
+
isProcessing: true,
|
|
908
|
+
}));
|
|
909
|
+
const analysis = await withTimeout(
|
|
910
|
+
plugin.analyzeQueryResults(
|
|
911
|
+
question,
|
|
912
|
+
query,
|
|
913
|
+
execResult.columns,
|
|
914
|
+
execResult.rows,
|
|
915
|
+
metadata,
|
|
916
|
+
config,
|
|
917
|
+
),
|
|
918
|
+
ANALYSIS_TIMEOUT_MS,
|
|
919
|
+
);
|
|
920
|
+
addThinkingStep(setMessages, 'Verifying answer coverage...');
|
|
921
|
+
if (analysis) {
|
|
922
|
+
const coverageNote = buildQuestionCoverageNote(
|
|
923
|
+
question,
|
|
924
|
+
execResult.columns,
|
|
925
|
+
);
|
|
926
|
+
const summary = coverageNote
|
|
927
|
+
? `${analysis.summary}\n\n${coverageNote}`
|
|
928
|
+
: analysis.summary;
|
|
929
|
+
completeThinkingSteps(setMessages);
|
|
930
|
+
updateLastAssistant(setMessages, () => ({
|
|
931
|
+
textAnswer: summary,
|
|
932
|
+
suggestedQueries: analysis.suggestedQueries,
|
|
933
|
+
isProcessing: false,
|
|
934
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
935
|
+
}));
|
|
936
|
+
} else {
|
|
937
|
+
const fallbackSummary = buildDeterministicResultSummary(
|
|
938
|
+
question,
|
|
939
|
+
query,
|
|
940
|
+
execResult.columns,
|
|
941
|
+
execResult.rows,
|
|
942
|
+
);
|
|
943
|
+
completeThinkingSteps(setMessages);
|
|
944
|
+
updateLastAssistant(setMessages, () => ({
|
|
945
|
+
textAnswer: fallbackSummary,
|
|
946
|
+
suggestedQueries: [],
|
|
947
|
+
isProcessing: false,
|
|
948
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
949
|
+
}));
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
async function handleEmptyOrchestratorResults(
|
|
954
|
+
question: string,
|
|
955
|
+
legendQuery: string,
|
|
956
|
+
orchestratorOptions: Required<LegendAIOrchestratorOptionsParam>,
|
|
957
|
+
metadata: LegendAIProductMetadata,
|
|
958
|
+
resolvedEntities: LegendAIResolvedEntities,
|
|
959
|
+
context: LegendAIOperationContext,
|
|
960
|
+
startTime: number,
|
|
961
|
+
): Promise<void> {
|
|
962
|
+
const { dataProductCoordinates, pureExecutionContext } = orchestratorOptions;
|
|
963
|
+
const { config, plugin, setMessages } = context;
|
|
964
|
+
|
|
965
|
+
if (resolvedEntities.relatedEntities.length > 0) {
|
|
966
|
+
const alternateRoot = resolvedEntities.relatedEntities[0];
|
|
967
|
+
if (alternateRoot) {
|
|
968
|
+
addThinkingStep(
|
|
969
|
+
setMessages,
|
|
970
|
+
`No results with ${extractElementNameFromPath(resolvedEntities.rootEntity)}, retrying with ${extractElementNameFromPath(alternateRoot)}...`,
|
|
971
|
+
);
|
|
972
|
+
|
|
973
|
+
try {
|
|
974
|
+
const retryResponse = await plugin.generateQueryViaOrchestrator(
|
|
975
|
+
{
|
|
976
|
+
user_question: question,
|
|
977
|
+
semantic_search_resolution_details: {
|
|
978
|
+
data_product_coordinates: dataProductCoordinates,
|
|
979
|
+
root_entity: alternateRoot,
|
|
980
|
+
related_entities: resolvedEntities.relatedEntities.slice(1),
|
|
981
|
+
},
|
|
982
|
+
},
|
|
983
|
+
config,
|
|
984
|
+
);
|
|
985
|
+
|
|
986
|
+
updateLastAssistant(setMessages, () => ({
|
|
987
|
+
sql: retryResponse.legend_query,
|
|
988
|
+
sqlGenTime: elapsedSeconds(startTime, 2),
|
|
989
|
+
isExecuting: true,
|
|
990
|
+
}));
|
|
991
|
+
|
|
992
|
+
const retryResult = await executePureQueryAndReport(
|
|
993
|
+
retryResponse.legend_query,
|
|
994
|
+
pureExecutionContext,
|
|
995
|
+
dataProductCoordinates,
|
|
996
|
+
config,
|
|
997
|
+
plugin,
|
|
998
|
+
setMessages,
|
|
999
|
+
startTime,
|
|
1000
|
+
);
|
|
1001
|
+
|
|
1002
|
+
if (retryResult.rows.length > 0) {
|
|
1003
|
+
await analyzeOrchestratorResults(
|
|
1004
|
+
question,
|
|
1005
|
+
retryResponse.legend_query,
|
|
1006
|
+
retryResult,
|
|
1007
|
+
metadata,
|
|
1008
|
+
context,
|
|
1009
|
+
startTime,
|
|
1010
|
+
);
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
} catch (retryError) {
|
|
1014
|
+
assertErrorThrown(retryError);
|
|
1015
|
+
addThinkingStep(
|
|
1016
|
+
setMessages,
|
|
1017
|
+
`Retry with alternate entity failed: ${retryError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
addThinkingStep(
|
|
1024
|
+
setMessages,
|
|
1025
|
+
'No results returned \u2014 building contextual guidance...',
|
|
1026
|
+
);
|
|
1027
|
+
updateLastAssistant(setMessages, () => ({
|
|
1028
|
+
isProcessing: true,
|
|
1029
|
+
}));
|
|
1030
|
+
const fallback = await withTimeout(
|
|
1031
|
+
plugin.buildNoResultsFallback(question, legendQuery, metadata, config),
|
|
1032
|
+
ANALYSIS_TIMEOUT_MS,
|
|
1033
|
+
);
|
|
1034
|
+
if (fallback) {
|
|
1035
|
+
completeThinkingSteps(setMessages);
|
|
1036
|
+
updateLastAssistant(setMessages, () => ({
|
|
1037
|
+
textAnswer: fallback.summary,
|
|
1038
|
+
suggestedQueries: fallback.suggestedQueries,
|
|
1039
|
+
isProcessing: false,
|
|
1040
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
1041
|
+
}));
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
export async function processQuestionViaOrchestrator(
|
|
1046
|
+
question: string,
|
|
1047
|
+
dataProductCoordinates: LegendAIOrchestratorDataProductCoordinates,
|
|
1048
|
+
metadata: LegendAIProductMetadata,
|
|
1049
|
+
context: LegendAIOperationContext,
|
|
1050
|
+
pureExecutionContext?: QueryExplicitExecutionContextInfo,
|
|
1051
|
+
preResolvedEntities?: LegendAIResolvedEntities,
|
|
1052
|
+
): Promise<void> {
|
|
1053
|
+
const { config, plugin, setMessages } = context;
|
|
1054
|
+
const startTime = Date.now();
|
|
1055
|
+
|
|
1056
|
+
try {
|
|
1057
|
+
let resolvedEntities: LegendAIResolvedEntities;
|
|
1058
|
+
if (preResolvedEntities) {
|
|
1059
|
+
resolvedEntities = preResolvedEntities;
|
|
1060
|
+
addThinkingStep(
|
|
1061
|
+
setMessages,
|
|
1062
|
+
`Using pre-resolved root entity: ${extractElementNameFromPath(resolvedEntities.rootEntity)}`,
|
|
1063
|
+
);
|
|
1064
|
+
} else {
|
|
1065
|
+
addThinkingStep(setMessages, 'Resolving entities for your query...');
|
|
1066
|
+
resolvedEntities = await plugin.resolveEntitiesForQuery(
|
|
1067
|
+
question,
|
|
1068
|
+
dataProductCoordinates,
|
|
1069
|
+
config,
|
|
1070
|
+
pureExecutionContext,
|
|
1071
|
+
);
|
|
1072
|
+
addThinkingStep(
|
|
1073
|
+
setMessages,
|
|
1074
|
+
`Found root entity: ${extractElementNameFromPath(resolvedEntities.rootEntity)}`,
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
if (resolvedEntities.relatedEntities.length > 0) {
|
|
1079
|
+
addThinkingStep(
|
|
1080
|
+
setMessages,
|
|
1081
|
+
`Found ${resolvedEntities.relatedEntities.length} related entities`,
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
addThinkingStep(setMessages, 'Generating Legend query via orchestrator...');
|
|
1086
|
+
const orchestratorResponse = await plugin.generateQueryViaOrchestrator(
|
|
1087
|
+
{
|
|
1088
|
+
user_question: question,
|
|
1089
|
+
semantic_search_resolution_details: {
|
|
1090
|
+
data_product_coordinates: dataProductCoordinates,
|
|
1091
|
+
root_entity: resolvedEntities.rootEntity,
|
|
1092
|
+
related_entities: resolvedEntities.relatedEntities,
|
|
1093
|
+
},
|
|
1094
|
+
},
|
|
1095
|
+
config,
|
|
1096
|
+
);
|
|
1097
|
+
|
|
1098
|
+
const queryGenTime = elapsedSeconds(startTime, 2);
|
|
1099
|
+
completeThinkingSteps(setMessages);
|
|
1100
|
+
updateLastAssistant(setMessages, () => ({
|
|
1101
|
+
sql: orchestratorResponse.legend_query,
|
|
1102
|
+
sqlGenTime: queryGenTime,
|
|
1103
|
+
isExecuting: true,
|
|
1104
|
+
isProcessing: true,
|
|
1105
|
+
}));
|
|
1106
|
+
|
|
1107
|
+
if (!pureExecutionContext) {
|
|
1108
|
+
updateLastAssistant(setMessages, () => ({
|
|
1109
|
+
isProcessing: false,
|
|
1110
|
+
isExecuting: false,
|
|
1111
|
+
error:
|
|
1112
|
+
'No execution context available — cannot execute query via engine.',
|
|
1113
|
+
errorType: LegendAIErrorType.EXECUTION,
|
|
1114
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
1115
|
+
}));
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const execResult = await executePureQueryAndReport(
|
|
1120
|
+
orchestratorResponse.legend_query,
|
|
1121
|
+
pureExecutionContext,
|
|
1122
|
+
dataProductCoordinates,
|
|
1123
|
+
config,
|
|
1124
|
+
plugin,
|
|
1125
|
+
setMessages,
|
|
1126
|
+
startTime,
|
|
1127
|
+
);
|
|
1128
|
+
|
|
1129
|
+
try {
|
|
1130
|
+
if (execResult.rows.length > 0) {
|
|
1131
|
+
await analyzeOrchestratorResults(
|
|
1132
|
+
question,
|
|
1133
|
+
orchestratorResponse.legend_query,
|
|
1134
|
+
execResult,
|
|
1135
|
+
metadata,
|
|
1136
|
+
context,
|
|
1137
|
+
startTime,
|
|
1138
|
+
);
|
|
1139
|
+
} else {
|
|
1140
|
+
await handleEmptyOrchestratorResults(
|
|
1141
|
+
question,
|
|
1142
|
+
orchestratorResponse.legend_query,
|
|
1143
|
+
{ dataProductCoordinates, pureExecutionContext },
|
|
1144
|
+
metadata,
|
|
1145
|
+
resolvedEntities,
|
|
1146
|
+
context,
|
|
1147
|
+
startTime,
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
} catch (analysisError) {
|
|
1151
|
+
assertErrorThrown(analysisError);
|
|
1152
|
+
addThinkingStep(
|
|
1153
|
+
setMessages,
|
|
1154
|
+
`Result analysis failed: ${analysisError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
1155
|
+
);
|
|
1156
|
+
} finally {
|
|
1157
|
+
completeThinkingSteps(setMessages);
|
|
1158
|
+
updateLastAssistant(setMessages, () => ({
|
|
1159
|
+
isProcessing: false,
|
|
1160
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
1161
|
+
}));
|
|
1162
|
+
}
|
|
1163
|
+
} catch (error) {
|
|
1164
|
+
assertErrorThrown(error);
|
|
1165
|
+
const orchErrorType = classifyError(error);
|
|
1166
|
+
addThinkingStep(
|
|
1167
|
+
setMessages,
|
|
1168
|
+
`Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
1169
|
+
);
|
|
1170
|
+
|
|
1171
|
+
try {
|
|
1172
|
+
addThinkingStep(
|
|
1173
|
+
setMessages,
|
|
1174
|
+
'Building guidance from available metadata...',
|
|
1175
|
+
);
|
|
1176
|
+
const fallbackText = await withTimeout(
|
|
1177
|
+
plugin.buildFailureFallback(question, error.message, metadata, config),
|
|
1178
|
+
ANALYSIS_TIMEOUT_MS,
|
|
1179
|
+
);
|
|
1180
|
+
if (fallbackText) {
|
|
1181
|
+
completeThinkingSteps(setMessages);
|
|
1182
|
+
updateLastAssistant(setMessages, () => ({
|
|
1183
|
+
dataContext: fallbackText,
|
|
1184
|
+
isProcessing: false,
|
|
1185
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
1186
|
+
}));
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
} catch (fallbackError) {
|
|
1190
|
+
assertErrorThrown(fallbackError);
|
|
1191
|
+
addThinkingStep(
|
|
1192
|
+
setMessages,
|
|
1193
|
+
`Fallback guidance failed: ${fallbackError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
finishWithThinkingError(
|
|
1198
|
+
setMessages,
|
|
1199
|
+
error.message,
|
|
1200
|
+
startTime,
|
|
1201
|
+
orchErrorType,
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
export function cleanLlmSqlResponse(raw: string): string {
|
|
1207
|
+
return raw
|
|
1208
|
+
.trim()
|
|
1209
|
+
.replace(/^```\w*\n?/, '')
|
|
1210
|
+
.replace(/\n?```$/, '')
|
|
1211
|
+
.replace(/;\s*$/, '')
|
|
1212
|
+
.trim();
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
export function isValidSqlCorrection(
|
|
1216
|
+
trimmed: string,
|
|
1217
|
+
currentSql: string,
|
|
1218
|
+
): boolean {
|
|
1219
|
+
return (
|
|
1220
|
+
trimmed.length > 0 &&
|
|
1221
|
+
trimmed.toLowerCase().startsWith('select') &&
|
|
1222
|
+
trimmed !== currentSql
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
export function sanitizeJoinOrderBy(sql: string): string {
|
|
1227
|
+
if (!JOIN_PATTERN.test(sql)) {
|
|
1228
|
+
return sql;
|
|
1229
|
+
}
|
|
1230
|
+
const parts = sql.split(ORDER_BY_SPLIT);
|
|
1231
|
+
if (parts.length < 2) {
|
|
1232
|
+
return sql;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
const beforeOrderBy = parts[0] ?? '';
|
|
1236
|
+
const afterOrderBy = parts.slice(1).join('ORDER BY').replace(/^\s+/, '');
|
|
1237
|
+
|
|
1238
|
+
const selectAliases = new Map<string, string>();
|
|
1239
|
+
const aliasRegex =
|
|
1240
|
+
/\b(?<tbl>[a-z]\w*)\s*\.\s*"(?<col>[^"]+)"\s+AS\s+(?:"(?<qAlias>[^"]+)"|(?<uAlias>\w+))/gi;
|
|
1241
|
+
let m: RegExpExecArray | null;
|
|
1242
|
+
while ((m = aliasRegex.exec(beforeOrderBy)) !== null) {
|
|
1243
|
+
const tableAlias = (m.groups?.tbl ?? '').toLowerCase();
|
|
1244
|
+
const colName = (m.groups?.col ?? '').toLowerCase();
|
|
1245
|
+
const asAlias = m.groups?.qAlias ?? m.groups?.uAlias ?? '';
|
|
1246
|
+
selectAliases.set(`${tableAlias}.${colName}`, asAlias);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
if (selectAliases.size === 0) {
|
|
1250
|
+
return sql;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
const rewritten = afterOrderBy.replaceAll(
|
|
1254
|
+
ALIAS_DOT_COL_PATTERN,
|
|
1255
|
+
(...args) => {
|
|
1256
|
+
const groups = args[args.length - 1] as {
|
|
1257
|
+
tbl: string;
|
|
1258
|
+
col: string;
|
|
1259
|
+
};
|
|
1260
|
+
const key = `${groups.tbl.toLowerCase()}.${groups.col.toLowerCase()}`;
|
|
1261
|
+
const alias = selectAliases.get(key);
|
|
1262
|
+
return alias ? `"${alias}"` : String(args[0]);
|
|
1263
|
+
},
|
|
1264
|
+
);
|
|
1265
|
+
|
|
1266
|
+
if (rewritten === afterOrderBy) {
|
|
1267
|
+
return sql;
|
|
1268
|
+
}
|
|
1269
|
+
return `${beforeOrderBy}ORDER BY ${rewritten}`;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
export function sanitizeLiteralColumns(sql: string): string {
|
|
1273
|
+
if (!UNION_ALL_PATTERN.test(sql)) {
|
|
1274
|
+
return sql;
|
|
1275
|
+
}
|
|
1276
|
+
LITERAL_COL_PATTERN.lastIndex = 0;
|
|
1277
|
+
if (!LITERAL_COL_PATTERN.test(sql)) {
|
|
1278
|
+
return sql;
|
|
1279
|
+
}
|
|
1280
|
+
LITERAL_COL_PATTERN.lastIndex = 0;
|
|
1281
|
+
return sql.replace(LITERAL_COL_PATTERN, '');
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function isDateLikeParam(paramName: string): boolean {
|
|
1285
|
+
return SERVICE_PARAM_DATE_LIKE_PATTERNS.some((p) => p.test(paramName));
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function hasUnresolvableParams(service: TDSServiceSchema): boolean {
|
|
1289
|
+
return service.parameters.some((p) => !isDateLikeParam(p));
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
function getNonDateParamNames(service: TDSServiceSchema): string[] {
|
|
1293
|
+
return service.parameters.filter((p) => !isDateLikeParam(p));
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* Strips non-date service parameters whose values were NOT explicitly
|
|
1298
|
+
* mentioned by the user in their question. Parameters with values that
|
|
1299
|
+
* appear in the question text are kept, since those are user-intended.
|
|
1300
|
+
*/
|
|
1301
|
+
export function stripGuessedNonDateServiceParams(
|
|
1302
|
+
sql: string,
|
|
1303
|
+
question: string,
|
|
1304
|
+
): string {
|
|
1305
|
+
const lowerQuestion = question.toLowerCase();
|
|
1306
|
+
return sql.replaceAll(/,\s*\w+\s*=>\s*'[^']*'/g, (match) => {
|
|
1307
|
+
const parts = /,\s*(?<param>\w+)\s*=>\s*'(?<value>[^']*)'/.exec(
|
|
1308
|
+
match,
|
|
1309
|
+
)?.groups;
|
|
1310
|
+
if (!parts?.param) {
|
|
1311
|
+
return match;
|
|
1312
|
+
}
|
|
1313
|
+
if (parts.param === 'coordinates' || isDateLikeParam(parts.param)) {
|
|
1314
|
+
return match;
|
|
1315
|
+
}
|
|
1316
|
+
if (parts.value && lowerQuestion.includes(parts.value.toLowerCase())) {
|
|
1317
|
+
return match;
|
|
1318
|
+
}
|
|
1319
|
+
return '';
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/**
|
|
1324
|
+
* Ensures all date-like parameters from the service schemas are present
|
|
1325
|
+
* in EVERY service() call in the SQL. If the LLM omitted a mandatory date
|
|
1326
|
+
* parameter, this injects it with today's date into each service() call
|
|
1327
|
+
* that lacks it. Only applies to service() calls (not p() calls).
|
|
1328
|
+
*/
|
|
1329
|
+
export function ensureDateParameters(
|
|
1330
|
+
sql: string,
|
|
1331
|
+
services: TDSServiceSchema[],
|
|
1332
|
+
): string {
|
|
1333
|
+
const dateParams = new Set<string>();
|
|
1334
|
+
for (const svc of services) {
|
|
1335
|
+
if (svc.sourceType === TDSServiceSourceType.ACCESS_POINT) {
|
|
1336
|
+
continue;
|
|
1337
|
+
}
|
|
1338
|
+
for (const p of svc.parameters) {
|
|
1339
|
+
if (isDateLikeParam(p)) {
|
|
1340
|
+
dateParams.add(p);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
if (dateParams.size === 0) {
|
|
1345
|
+
return sql;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
const today = getTodayISO();
|
|
1349
|
+
|
|
1350
|
+
const serviceCallPattern = /\bservice\s*\([^()]*\)/gi;
|
|
1351
|
+
return sql.replaceAll(serviceCallPattern, (match) => {
|
|
1352
|
+
let patched = match;
|
|
1353
|
+
for (const param of dateParams) {
|
|
1354
|
+
if (new RegExp(String.raw`\b${param}\s*=>`, 'i').test(patched)) {
|
|
1355
|
+
continue;
|
|
1356
|
+
}
|
|
1357
|
+
const lastParen = patched.lastIndexOf(')');
|
|
1358
|
+
if (lastParen !== -1) {
|
|
1359
|
+
patched = `${patched.slice(
|
|
1360
|
+
0,
|
|
1361
|
+
lastParen,
|
|
1362
|
+
)},\n ${param} => '${today}'${patched.slice(lastParen)}`;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
return patched;
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
export interface MissingParamInfo {
|
|
1370
|
+
name: string;
|
|
1371
|
+
hint?: string;
|
|
1372
|
+
isDateLike: boolean;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
function resolveParamHint(
|
|
1376
|
+
paramName: string,
|
|
1377
|
+
isDateLike: boolean,
|
|
1378
|
+
service: TDSServiceSchema,
|
|
1379
|
+
): string | undefined {
|
|
1380
|
+
if (isDateLike) {
|
|
1381
|
+
return `today's date: ${getTodayISO()}`;
|
|
1382
|
+
}
|
|
1383
|
+
const matchingCol = service.columns.find((c) => c.name === paramName);
|
|
1384
|
+
return matchingCol?.sampleValues ?? matchingCol?.documentation ?? undefined;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
/**
|
|
1388
|
+
* Detects ALL service parameters required by the schema but missing from
|
|
1389
|
+
* the generated SQL. Works for any parameter type — date, identifier, key,
|
|
1390
|
+
* or anything else. Each result includes a hint (from schema column docs
|
|
1391
|
+
* or sample values) and whether the param is date-like.
|
|
1392
|
+
*/
|
|
1393
|
+
export function detectMissingServiceParams(
|
|
1394
|
+
sql: string,
|
|
1395
|
+
services: TDSServiceSchema[],
|
|
1396
|
+
): MissingParamInfo[] {
|
|
1397
|
+
const missing: MissingParamInfo[] = [];
|
|
1398
|
+
const seen = new Set<string>();
|
|
1399
|
+
|
|
1400
|
+
for (const svc of services) {
|
|
1401
|
+
if (svc.sourceType === TDSServiceSourceType.ACCESS_POINT) {
|
|
1402
|
+
continue;
|
|
1403
|
+
}
|
|
1404
|
+
for (const p of svc.parameters) {
|
|
1405
|
+
if (seen.has(p)) {
|
|
1406
|
+
continue;
|
|
1407
|
+
}
|
|
1408
|
+
seen.add(p);
|
|
1409
|
+
if (!new RegExp(String.raw`\b${p}\s*=>`, 'i').test(sql)) {
|
|
1410
|
+
const isDateLike = isDateLikeParam(p);
|
|
1411
|
+
const hint = resolveParamHint(p, isDateLike, svc);
|
|
1412
|
+
missing.push({
|
|
1413
|
+
name: p,
|
|
1414
|
+
isDateLike,
|
|
1415
|
+
...(hint === undefined ? {} : { hint }),
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
return missing;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
/**
|
|
1424
|
+
* Builds a user-facing warning message listing which service parameters
|
|
1425
|
+
* are missing from the query and need to be provided by the user.
|
|
1426
|
+
*/
|
|
1427
|
+
export function buildMissingParamsWarning(
|
|
1428
|
+
missingParams: MissingParamInfo[],
|
|
1429
|
+
): string {
|
|
1430
|
+
const paramDescriptions = missingParams.map((mp) => {
|
|
1431
|
+
if (mp.hint) {
|
|
1432
|
+
return `- **${mp.name}** (e.g. ${mp.hint})`;
|
|
1433
|
+
}
|
|
1434
|
+
return `- **${mp.name}**`;
|
|
1435
|
+
});
|
|
1436
|
+
const exampleValues = missingParams
|
|
1437
|
+
.map((mp) => {
|
|
1438
|
+
if (mp.isDateLike) {
|
|
1439
|
+
return `${mp.name}=${getTodayISO()}`;
|
|
1440
|
+
}
|
|
1441
|
+
return `${mp.name}=[your value]`;
|
|
1442
|
+
})
|
|
1443
|
+
.join(', ');
|
|
1444
|
+
return [
|
|
1445
|
+
`This service requires the following parameter${missingParams.length > 1 ? 's' : ''} to execute:`,
|
|
1446
|
+
'',
|
|
1447
|
+
...paramDescriptions,
|
|
1448
|
+
'',
|
|
1449
|
+
`Please provide ${missingParams.length > 1 ? 'values' : 'a value'} in your question, e.g. "show data where ${exampleValues}".`,
|
|
1450
|
+
].join('\n');
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
/**
|
|
1454
|
+
* Appends a safety LIMIT to queries that lack one, preventing unbounded
|
|
1455
|
+
* result sets on large services. Skips aggregation queries since those
|
|
1456
|
+
* naturally produce bounded output.
|
|
1457
|
+
*/
|
|
1458
|
+
export function ensureSafeLimit(
|
|
1459
|
+
sql: string,
|
|
1460
|
+
limit: number = DEFAULT_SAFETY_LIMIT,
|
|
1461
|
+
): string {
|
|
1462
|
+
if (HAS_LIMIT_PATTERN.test(sql) || HAS_AGGREGATION_PATTERN.test(sql)) {
|
|
1463
|
+
return sql;
|
|
1464
|
+
}
|
|
1465
|
+
return `${sql.trimEnd()}\nLIMIT ${limit}`;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
async function executeSqlForServices(
|
|
1469
|
+
sql: string,
|
|
1470
|
+
services: TDSServiceSchema[],
|
|
1471
|
+
dataProductCoordinates:
|
|
1472
|
+
| LegendAIOrchestratorDataProductCoordinates
|
|
1473
|
+
| undefined,
|
|
1474
|
+
plugin: LegendAI_LegendApplicationPlugin_Extension,
|
|
1475
|
+
config: LegendAIConfig,
|
|
1476
|
+
): Promise<LegendAISqlExecutionResultData> {
|
|
1477
|
+
const safeSql = ensureSafeLimit(
|
|
1478
|
+
ensureDateParameters(
|
|
1479
|
+
sanitizeLiteralColumns(sanitizeJoinOrderBy(sql)),
|
|
1480
|
+
services,
|
|
1481
|
+
),
|
|
1482
|
+
);
|
|
1483
|
+
const isAccessPoint = services.some(
|
|
1484
|
+
(s) => s.sourceType === TDSServiceSourceType.ACCESS_POINT,
|
|
1485
|
+
);
|
|
1486
|
+
if (isAccessPoint && dataProductCoordinates) {
|
|
1487
|
+
return plugin.executeLakehouseSql(safeSql, dataProductCoordinates, config);
|
|
1488
|
+
}
|
|
1489
|
+
return plugin.executeSql(safeSql, config);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
interface SqlExecutionOutcome {
|
|
1493
|
+
sql: string;
|
|
1494
|
+
result?: LegendAISqlExecutionResultData;
|
|
1495
|
+
error?: string;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
async function executeSqlWithRetries(
|
|
1499
|
+
initialSql: string,
|
|
1500
|
+
question: string,
|
|
1501
|
+
services: TDSServiceSchema[],
|
|
1502
|
+
coordinates: string,
|
|
1503
|
+
dataProductCoordinates:
|
|
1504
|
+
| LegendAIOrchestratorDataProductCoordinates
|
|
1505
|
+
| undefined,
|
|
1506
|
+
context: LegendAIOperationContext,
|
|
1507
|
+
): Promise<SqlExecutionOutcome> {
|
|
1508
|
+
const { plugin, config, setMessages } = context;
|
|
1509
|
+
let currentSql = initialSql;
|
|
1510
|
+
|
|
1511
|
+
for (let attempt = 0; attempt <= DEFAULT_MAX_EXECUTION_RETRIES; attempt++) {
|
|
1512
|
+
try {
|
|
1513
|
+
const result = await executeSqlForServices(
|
|
1514
|
+
currentSql,
|
|
1515
|
+
services,
|
|
1516
|
+
dataProductCoordinates,
|
|
1517
|
+
plugin,
|
|
1518
|
+
config,
|
|
1519
|
+
);
|
|
1520
|
+
return { sql: currentSql, result };
|
|
1521
|
+
} catch (executeError) {
|
|
1522
|
+
assertErrorThrown(executeError);
|
|
1523
|
+
if (attempt >= DEFAULT_MAX_EXECUTION_RETRIES) {
|
|
1524
|
+
return { sql: currentSql, error: executeError.message };
|
|
1525
|
+
}
|
|
1526
|
+
addThinkingStep(
|
|
1527
|
+
setMessages,
|
|
1528
|
+
`Execution failed (attempt ${attempt + 1}/${DEFAULT_MAX_EXECUTION_RETRIES + 1}), correcting query...`,
|
|
1529
|
+
);
|
|
1530
|
+
const corrected = await attemptErrorCorrection(
|
|
1531
|
+
currentSql,
|
|
1532
|
+
executeError.message,
|
|
1533
|
+
question,
|
|
1534
|
+
services,
|
|
1535
|
+
coordinates,
|
|
1536
|
+
context,
|
|
1537
|
+
);
|
|
1538
|
+
if (corrected) {
|
|
1539
|
+
currentSql = corrected;
|
|
1540
|
+
updateLastAssistant(setMessages, () => ({ sql: currentSql }));
|
|
1541
|
+
continue;
|
|
1542
|
+
}
|
|
1543
|
+
return { sql: currentSql, error: executeError.message };
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
return { sql: currentSql };
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
async function attemptErrorCorrection(
|
|
1550
|
+
currentSql: string,
|
|
1551
|
+
errorMessage: string,
|
|
1552
|
+
question: string,
|
|
1553
|
+
services: TDSServiceSchema[],
|
|
1554
|
+
coordinates: string,
|
|
1555
|
+
context: LegendAIOperationContext,
|
|
1556
|
+
): Promise<string | undefined> {
|
|
1557
|
+
const { plugin, config, setMessages } = context;
|
|
1558
|
+
|
|
1559
|
+
let availableColumns: string[] | undefined;
|
|
1560
|
+
if (/no column found|column.*not.*found/i.test(errorMessage)) {
|
|
1561
|
+
const primaryService = services[0];
|
|
1562
|
+
if (primaryService) {
|
|
1563
|
+
availableColumns = await plugin.probeServiceColumns(
|
|
1564
|
+
primaryService,
|
|
1565
|
+
coordinates,
|
|
1566
|
+
config,
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
const prompt = plugin.buildErrorCorrectionPrompt(
|
|
1572
|
+
currentSql,
|
|
1573
|
+
errorMessage,
|
|
1574
|
+
question,
|
|
1575
|
+
services,
|
|
1576
|
+
coordinates,
|
|
1577
|
+
availableColumns,
|
|
1578
|
+
);
|
|
1579
|
+
if (!prompt) {
|
|
1580
|
+
return undefined;
|
|
1581
|
+
}
|
|
1582
|
+
try {
|
|
1583
|
+
const correctedSql = await plugin.callLLM(prompt, config);
|
|
1584
|
+
const trimmed = cleanLlmSqlResponse(correctedSql);
|
|
1585
|
+
if (isValidSqlCorrection(trimmed, currentSql)) {
|
|
1586
|
+
return trimmed;
|
|
1587
|
+
}
|
|
1588
|
+
} catch (correctionError) {
|
|
1589
|
+
assertErrorThrown(correctionError);
|
|
1590
|
+
addThinkingStep(
|
|
1591
|
+
setMessages,
|
|
1592
|
+
`SQL error correction failed: ${correctionError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
1593
|
+
);
|
|
1594
|
+
}
|
|
1595
|
+
return undefined;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
async function attemptZeroRowCorrection(
|
|
1599
|
+
currentSql: string,
|
|
1600
|
+
question: string,
|
|
1601
|
+
services: TDSServiceSchema[],
|
|
1602
|
+
coordinates: string,
|
|
1603
|
+
dataProductCoordinates:
|
|
1604
|
+
| LegendAIOrchestratorDataProductCoordinates
|
|
1605
|
+
| undefined,
|
|
1606
|
+
context: LegendAIOperationContext,
|
|
1607
|
+
): Promise<
|
|
1608
|
+
{ sql: string; result: LegendAISqlExecutionResultData } | undefined
|
|
1609
|
+
> {
|
|
1610
|
+
const { plugin, config, setMessages } = context;
|
|
1611
|
+
addThinkingStep(
|
|
1612
|
+
setMessages,
|
|
1613
|
+
'Query returned 0 rows, attempting filter correction...',
|
|
1614
|
+
);
|
|
1615
|
+
const prompt = plugin.buildZeroRowCorrectionPrompt(
|
|
1616
|
+
currentSql,
|
|
1617
|
+
question,
|
|
1618
|
+
services,
|
|
1619
|
+
coordinates,
|
|
1620
|
+
);
|
|
1621
|
+
if (!prompt) {
|
|
1622
|
+
return undefined;
|
|
1623
|
+
}
|
|
1624
|
+
try {
|
|
1625
|
+
const correctedSql = await plugin.callLLM(prompt, config);
|
|
1626
|
+
const trimmed = cleanLlmSqlResponse(correctedSql);
|
|
1627
|
+
if (!isValidSqlCorrection(trimmed, currentSql)) {
|
|
1628
|
+
return undefined;
|
|
1629
|
+
}
|
|
1630
|
+
addThinkingStep(setMessages, 'Retrying with corrected filters...');
|
|
1631
|
+
updateLastAssistant(setMessages, () => ({ sql: trimmed }));
|
|
1632
|
+
try {
|
|
1633
|
+
const retryResult = await executeSqlForServices(
|
|
1634
|
+
trimmed,
|
|
1635
|
+
services,
|
|
1636
|
+
dataProductCoordinates,
|
|
1637
|
+
plugin,
|
|
1638
|
+
config,
|
|
1639
|
+
);
|
|
1640
|
+
if (retryResult.rows.length > 0) {
|
|
1641
|
+
return { sql: trimmed, result: retryResult };
|
|
1642
|
+
}
|
|
1643
|
+
} catch (retryExecError) {
|
|
1644
|
+
assertErrorThrown(retryExecError);
|
|
1645
|
+
addThinkingStep(
|
|
1646
|
+
setMessages,
|
|
1647
|
+
`Corrected query execution failed: ${retryExecError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
1648
|
+
);
|
|
1649
|
+
}
|
|
1650
|
+
} catch (correctionError) {
|
|
1651
|
+
assertErrorThrown(correctionError);
|
|
1652
|
+
addThinkingStep(
|
|
1653
|
+
setMessages,
|
|
1654
|
+
`Zero-row correction failed: ${correctionError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
1655
|
+
);
|
|
1656
|
+
}
|
|
1657
|
+
return undefined;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
function formatUnresolvableParams(services: TDSServiceSchema[]): string[] {
|
|
1661
|
+
const parts: string[] = [];
|
|
1662
|
+
for (const svc of services) {
|
|
1663
|
+
for (const paramName of getNonDateParamNames(svc)) {
|
|
1664
|
+
const matchingCol = svc.columns.find((c) => c.name === paramName);
|
|
1665
|
+
const docHint = matchingCol?.documentation ?? matchingCol?.sampleValues;
|
|
1666
|
+
parts.push(
|
|
1667
|
+
docHint ? `**${paramName}** (${docHint})` : `**${paramName}**`,
|
|
1668
|
+
);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
return [...new Set(parts)];
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
function buildZeroRowMessage(services: TDSServiceSchema[]): string {
|
|
1675
|
+
const withUnresolvable = services.filter((s) => hasUnresolvableParams(s));
|
|
1676
|
+
if (withUnresolvable.length > 0) {
|
|
1677
|
+
const uniqueParts = formatUnresolvableParams(withUnresolvable);
|
|
1678
|
+
const firstSvc = withUnresolvable[0];
|
|
1679
|
+
const firstParam = firstSvc
|
|
1680
|
+
? (getNonDateParamNames(firstSvc)[0] ?? 'parameter')
|
|
1681
|
+
: 'parameter';
|
|
1682
|
+
return `The SQL query executed successfully but returned **0 rows**. This service requires specific values for ${uniqueParts.join(', ')} to return data. Please include ${uniqueParts.length === 1 ? 'a value' : 'values'} in your question, e.g., "show data where ${firstParam} is [your value]".`;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
const withFailedExtraction = services.filter(
|
|
1686
|
+
(s) => s.parameterExtractionFailed,
|
|
1687
|
+
);
|
|
1688
|
+
if (withFailedExtraction.length > 0) {
|
|
1689
|
+
const names = withFailedExtraction.map((s) => `**${s.title}**`).join(', ');
|
|
1690
|
+
return `The SQL query executed successfully but returned **0 rows**. Note: parameter detection for ${names} was incomplete — this service may require additional parameters not shown in the schema. Try specifying filter values (dates, IDs, etc.) directly in your question.`;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
return 'The SQL query executed successfully but returned **0 rows**. The applied filters may not match any records, or the specific values may not exist in the queried datasets.';
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
function handleSqlGenerationFailure(
|
|
1697
|
+
setMessages: MessageSetter,
|
|
1698
|
+
startTime: number,
|
|
1699
|
+
hasOrchestratorFallback: boolean,
|
|
1700
|
+
orchestratorMessage: string,
|
|
1701
|
+
errorMessage: string,
|
|
1702
|
+
errorType: LegendAIErrorType,
|
|
1703
|
+
suggestedQueries?: string[],
|
|
1704
|
+
): void {
|
|
1705
|
+
const suggestions = suggestedQueries ?? [];
|
|
1706
|
+
completeThinkingSteps(setMessages);
|
|
1707
|
+
if (hasOrchestratorFallback) {
|
|
1708
|
+
updateLastAssistant(setMessages, () => ({
|
|
1709
|
+
textAnswer: orchestratorMessage,
|
|
1710
|
+
suggestedQueries: suggestions,
|
|
1711
|
+
fallbackAction: {
|
|
1712
|
+
label: ORCHESTRATOR_FALLBACK_LABEL,
|
|
1713
|
+
actionId: LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
|
|
1714
|
+
},
|
|
1715
|
+
isProcessing: false,
|
|
1716
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
1717
|
+
}));
|
|
1718
|
+
} else {
|
|
1719
|
+
finishWithThinkingError(setMessages, errorMessage, startTime, errorType);
|
|
1720
|
+
if (suggestions.length > 0) {
|
|
1721
|
+
updateLastAssistant(setMessages, () => ({
|
|
1722
|
+
suggestedQueries: suggestions,
|
|
1723
|
+
}));
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
interface QueryResultReport {
|
|
1729
|
+
currentSql: string;
|
|
1730
|
+
sqlResult: LegendAISqlExecutionResultData;
|
|
1731
|
+
question: string;
|
|
1732
|
+
services: TDSServiceSchema[];
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
async function reportQueryResults(
|
|
1736
|
+
report: QueryResultReport,
|
|
1737
|
+
metadata: LegendAIProductMetadata,
|
|
1738
|
+
context: LegendAIOperationContext,
|
|
1739
|
+
startTime: number,
|
|
1740
|
+
hasOrchestratorFallback: boolean,
|
|
1741
|
+
): Promise<void> {
|
|
1742
|
+
const { currentSql, sqlResult, question, services } = report;
|
|
1743
|
+
const { setMessages } = context;
|
|
1744
|
+
if (sqlResult.rows.length > 0) {
|
|
1745
|
+
const columns = deduplicateColumns(sqlResult.columns);
|
|
1746
|
+
const rows = sqlResult.rows;
|
|
1747
|
+
completeThinkingSteps(setMessages);
|
|
1748
|
+
addThinkingStep(
|
|
1749
|
+
setMessages,
|
|
1750
|
+
`Retrieved ${rows.length} row${rows.length === 1 ? '' : 's'}`,
|
|
1751
|
+
);
|
|
1752
|
+
completeThinkingSteps(setMessages);
|
|
1753
|
+
updateLastAssistant(setMessages, () => ({
|
|
1754
|
+
sql: currentSql,
|
|
1755
|
+
gridData: {
|
|
1756
|
+
columnDefs: buildColumnDefsFromNames(columns),
|
|
1757
|
+
rowData: rows,
|
|
1758
|
+
},
|
|
1759
|
+
execTime: elapsedSeconds(startTime, 2),
|
|
1760
|
+
isProcessing: true,
|
|
1761
|
+
isExecuting: false,
|
|
1762
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
1763
|
+
}));
|
|
1764
|
+
|
|
1765
|
+
try {
|
|
1766
|
+
await analyzeOrchestratorResults(
|
|
1767
|
+
question,
|
|
1768
|
+
currentSql,
|
|
1769
|
+
sqlResult,
|
|
1770
|
+
metadata,
|
|
1771
|
+
context,
|
|
1772
|
+
startTime,
|
|
1773
|
+
);
|
|
1774
|
+
} catch (analysisError) {
|
|
1775
|
+
assertErrorThrown(analysisError);
|
|
1776
|
+
addThinkingStep(
|
|
1777
|
+
setMessages,
|
|
1778
|
+
`Result analysis failed: ${analysisError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
1779
|
+
);
|
|
1780
|
+
} finally {
|
|
1781
|
+
completeThinkingSteps(setMessages);
|
|
1782
|
+
updateLastAssistant(setMessages, () => ({
|
|
1783
|
+
isProcessing: false,
|
|
1784
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
1785
|
+
}));
|
|
1786
|
+
}
|
|
1787
|
+
} else {
|
|
1788
|
+
addThinkingStep(
|
|
1789
|
+
setMessages,
|
|
1790
|
+
'Query returned 0 rows after correction attempts.',
|
|
1791
|
+
);
|
|
1792
|
+
completeThinkingSteps(setMessages);
|
|
1793
|
+
const fallback = hasOrchestratorFallback
|
|
1794
|
+
? {
|
|
1795
|
+
fallbackAction: {
|
|
1796
|
+
label: ORCHESTRATOR_FALLBACK_LABEL,
|
|
1797
|
+
actionId: LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
|
|
1798
|
+
},
|
|
1799
|
+
}
|
|
1800
|
+
: {};
|
|
1801
|
+
updateLastAssistant(setMessages, () => ({
|
|
1802
|
+
textAnswer: buildZeroRowMessage(services),
|
|
1803
|
+
...fallback,
|
|
1804
|
+
isProcessing: false,
|
|
1805
|
+
isExecuting: false,
|
|
1806
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
1807
|
+
}));
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
/**
|
|
1812
|
+
* Scores each service against the user question by counting keyword
|
|
1813
|
+
* overlap between the question tokens and the service title, description,
|
|
1814
|
+
* column names, and parameter names. Returns services sorted by
|
|
1815
|
+
* descending relevance score.
|
|
1816
|
+
*/
|
|
1817
|
+
export function preFilterServicesByRelevance(
|
|
1818
|
+
question: string,
|
|
1819
|
+
services: TDSServiceSchema[],
|
|
1820
|
+
limit: number,
|
|
1821
|
+
): TDSServiceSchema[] {
|
|
1822
|
+
const queryTokens = question
|
|
1823
|
+
.toLowerCase()
|
|
1824
|
+
.split(/[\s,.:;!?'"()\-/]+/)
|
|
1825
|
+
.filter((t) => t.length > 2);
|
|
1826
|
+
if (queryTokens.length === 0) {
|
|
1827
|
+
return services.slice(0, limit);
|
|
1828
|
+
}
|
|
1829
|
+
const scored = services.map((svc) => {
|
|
1830
|
+
const haystack = [
|
|
1831
|
+
svc.title,
|
|
1832
|
+
svc.description ?? '',
|
|
1833
|
+
svc.pattern,
|
|
1834
|
+
...svc.columns.slice(0, 30).map((c) => c.name),
|
|
1835
|
+
...svc.parameters,
|
|
1836
|
+
...(svc.preFilters ?? []).flatMap((pf) => {
|
|
1837
|
+
const parts = [pf.property, pf.operator];
|
|
1838
|
+
if (pf.value !== undefined) {
|
|
1839
|
+
parts.push(String(pf.value));
|
|
1840
|
+
}
|
|
1841
|
+
return parts;
|
|
1842
|
+
}),
|
|
1843
|
+
]
|
|
1844
|
+
.join(' ')
|
|
1845
|
+
.toLowerCase();
|
|
1846
|
+
let score = 0;
|
|
1847
|
+
for (const token of queryTokens) {
|
|
1848
|
+
if (haystack.includes(token)) {
|
|
1849
|
+
score += 1;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
return { svc, score };
|
|
1853
|
+
});
|
|
1854
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1855
|
+
return scored.slice(0, limit).map((s) => s.svc);
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
async function selectBestServices(
|
|
1859
|
+
question: string,
|
|
1860
|
+
services: TDSServiceSchema[],
|
|
1861
|
+
context: LegendAIOperationContext,
|
|
1862
|
+
): Promise<TDSServiceSchema[]> {
|
|
1863
|
+
const { plugin, config, setMessages } = context;
|
|
1864
|
+
if (services.length <= 1) {
|
|
1865
|
+
return services;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
let candidates = services;
|
|
1869
|
+
if (services.length > MAX_SERVICES_FOR_LLM_SELECTION) {
|
|
1870
|
+
candidates = preFilterServicesByRelevance(
|
|
1871
|
+
question,
|
|
1872
|
+
services,
|
|
1873
|
+
MAX_SERVICES_FOR_LLM_SELECTION,
|
|
1874
|
+
);
|
|
1875
|
+
addThinkingStep(
|
|
1876
|
+
setMessages,
|
|
1877
|
+
`Pre-filtered ${services.length} services to ${candidates.length} candidates`,
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
try {
|
|
1882
|
+
addThinkingStep(setMessages, 'Selecting best service for your query...');
|
|
1883
|
+
return await plugin.selectRelevantServices(question, candidates, config);
|
|
1884
|
+
} catch (selectionError) {
|
|
1885
|
+
assertErrorThrown(selectionError);
|
|
1886
|
+
addThinkingStep(
|
|
1887
|
+
setMessages,
|
|
1888
|
+
`Service selection failed, using pre-filtered candidates: ${selectionError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
1889
|
+
);
|
|
1890
|
+
return candidates;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
async function tryRecoverZeroRows(
|
|
1895
|
+
currentSql: string,
|
|
1896
|
+
sqlResult: LegendAISqlExecutionResultData,
|
|
1897
|
+
question: string,
|
|
1898
|
+
selectedServices: TDSServiceSchema[],
|
|
1899
|
+
coordinates: string,
|
|
1900
|
+
dataProductCoordinates:
|
|
1901
|
+
| LegendAIOrchestratorDataProductCoordinates
|
|
1902
|
+
| undefined,
|
|
1903
|
+
context: LegendAIOperationContext,
|
|
1904
|
+
): Promise<{ sql: string; result: LegendAISqlExecutionResultData }> {
|
|
1905
|
+
const { plugin, config, setMessages } = context;
|
|
1906
|
+
let recoveredSql = currentSql;
|
|
1907
|
+
let recoveredResult = sqlResult;
|
|
1908
|
+
|
|
1909
|
+
const strippedSql = stripGuessedNonDateServiceParams(recoveredSql, question);
|
|
1910
|
+
if (strippedSql !== recoveredSql) {
|
|
1911
|
+
addThinkingStep(
|
|
1912
|
+
setMessages,
|
|
1913
|
+
'Trying query without guessed parameter values...',
|
|
1914
|
+
);
|
|
1915
|
+
try {
|
|
1916
|
+
const strippedResult = await executeSqlForServices(
|
|
1917
|
+
strippedSql,
|
|
1918
|
+
selectedServices,
|
|
1919
|
+
dataProductCoordinates,
|
|
1920
|
+
plugin,
|
|
1921
|
+
config,
|
|
1922
|
+
);
|
|
1923
|
+
if (strippedResult.rows.length > 0) {
|
|
1924
|
+
recoveredSql = strippedSql;
|
|
1925
|
+
recoveredResult = strippedResult;
|
|
1926
|
+
updateLastAssistant(setMessages, () => ({ sql: strippedSql }));
|
|
1927
|
+
}
|
|
1928
|
+
} catch (stripError) {
|
|
1929
|
+
assertErrorThrown(stripError);
|
|
1930
|
+
addThinkingStep(
|
|
1931
|
+
setMessages,
|
|
1932
|
+
`Parameter stripping recovery failed: ${stripError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
1933
|
+
);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
if (recoveredResult.rows.length === 0) {
|
|
1938
|
+
const correction = await attemptZeroRowCorrection(
|
|
1939
|
+
recoveredSql,
|
|
1940
|
+
question,
|
|
1941
|
+
selectedServices,
|
|
1942
|
+
coordinates,
|
|
1943
|
+
dataProductCoordinates,
|
|
1944
|
+
context,
|
|
1945
|
+
);
|
|
1946
|
+
if (correction) {
|
|
1947
|
+
recoveredSql = correction.sql;
|
|
1948
|
+
recoveredResult = correction.result;
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
return { sql: recoveredSql, result: recoveredResult };
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
/**
|
|
1956
|
+
* Dedicated query flow for data product access points.
|
|
1957
|
+
* Unlike processDataQuery this:
|
|
1958
|
+
* - Uses AP-specific generator/judge prompts (p() syntax, no coordinates/params)
|
|
1959
|
+
* - Skips parameter detection (APs have no parameters)
|
|
1960
|
+
*/
|
|
1961
|
+
async function processAccessPointQuery(
|
|
1962
|
+
question: string,
|
|
1963
|
+
accessPoints: TDSServiceSchema[],
|
|
1964
|
+
metadata: LegendAIProductMetadata,
|
|
1965
|
+
context: LegendAIOperationContext,
|
|
1966
|
+
startTime: number,
|
|
1967
|
+
dataProductCoordinates: LegendAIOrchestratorDataProductCoordinates,
|
|
1968
|
+
): Promise<void> {
|
|
1969
|
+
const { config, plugin, setMessages } = context;
|
|
1970
|
+
const hasOrchestratorFallback = Boolean(config.orchestratorUrl);
|
|
1971
|
+
|
|
1972
|
+
addThinkingStep(setMessages, 'Found relevant access points to query');
|
|
1973
|
+
|
|
1974
|
+
const selectedAPs = await selectBestServices(question, accessPoints, context);
|
|
1975
|
+
|
|
1976
|
+
const judgedSql = await generateAndJudgeAccessPointSql(
|
|
1977
|
+
question,
|
|
1978
|
+
selectedAPs,
|
|
1979
|
+
context,
|
|
1980
|
+
startTime,
|
|
1981
|
+
);
|
|
1982
|
+
|
|
1983
|
+
if (!judgedSql) {
|
|
1984
|
+
addThinkingStep(
|
|
1985
|
+
setMessages,
|
|
1986
|
+
'SQL generation could not produce a valid query.',
|
|
1987
|
+
);
|
|
1988
|
+
handleSqlGenerationFailure(
|
|
1989
|
+
setMessages,
|
|
1990
|
+
startTime,
|
|
1991
|
+
hasOrchestratorFallback,
|
|
1992
|
+
SQL_GENERATION_FAILURE_WITH_ORCHESTRATOR,
|
|
1993
|
+
SQL_GENERATION_FAILURE_NO_ORCHESTRATOR,
|
|
1994
|
+
LegendAIErrorType.GENERATION,
|
|
1995
|
+
buildFallbackSuggestions(selectedAPs),
|
|
1996
|
+
);
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
const sqlGenTimeValue = elapsedSeconds(startTime, 2);
|
|
2001
|
+
completeThinkingSteps(setMessages);
|
|
2002
|
+
updateLastAssistant(setMessages, () => ({
|
|
2003
|
+
sql: judgedSql,
|
|
2004
|
+
sqlGenTime: sqlGenTimeValue,
|
|
2005
|
+
isExecuting: true,
|
|
2006
|
+
}));
|
|
2007
|
+
|
|
2008
|
+
const execStartTime = Date.now();
|
|
2009
|
+
try {
|
|
2010
|
+
const safeSql = ensureSafeLimit(judgedSql);
|
|
2011
|
+
const rawResult = await plugin.executeLakehouseSql(
|
|
2012
|
+
safeSql,
|
|
2013
|
+
dataProductCoordinates,
|
|
2014
|
+
config,
|
|
2015
|
+
);
|
|
2016
|
+
|
|
2017
|
+
await reportQueryResults(
|
|
2018
|
+
{
|
|
2019
|
+
currentSql: judgedSql,
|
|
2020
|
+
sqlResult: rawResult,
|
|
2021
|
+
question,
|
|
2022
|
+
services: selectedAPs,
|
|
2023
|
+
},
|
|
2024
|
+
metadata,
|
|
2025
|
+
context,
|
|
2026
|
+
startTime,
|
|
2027
|
+
hasOrchestratorFallback,
|
|
2028
|
+
);
|
|
2029
|
+
} catch (executeError) {
|
|
2030
|
+
assertErrorThrown(executeError);
|
|
2031
|
+
const execErrorType = classifyError(executeError);
|
|
2032
|
+
addThinkingStep(
|
|
2033
|
+
setMessages,
|
|
2034
|
+
`Execution failed: ${executeError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
2035
|
+
);
|
|
2036
|
+
finishWithThinkingError(
|
|
2037
|
+
setMessages,
|
|
2038
|
+
buildExecutionErrorMessage(executeError.message, selectedAPs),
|
|
2039
|
+
startTime,
|
|
2040
|
+
execErrorType === LegendAIErrorType.GENERAL
|
|
2041
|
+
? LegendAIErrorType.EXECUTION
|
|
2042
|
+
: execErrorType,
|
|
2043
|
+
);
|
|
2044
|
+
updateLastAssistant(setMessages, () => ({
|
|
2045
|
+
execTime: elapsedSeconds(execStartTime, 2),
|
|
2046
|
+
isExecuting: false,
|
|
2047
|
+
suggestedQueries: buildFallbackSuggestions(selectedAPs),
|
|
2048
|
+
...(hasOrchestratorFallback
|
|
2049
|
+
? {
|
|
2050
|
+
fallbackAction: {
|
|
2051
|
+
label: ORCHESTRATOR_FALLBACK_LABEL,
|
|
2052
|
+
actionId: LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
|
|
2053
|
+
},
|
|
2054
|
+
}
|
|
2055
|
+
: {}),
|
|
2056
|
+
}));
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
async function processDataQuery(
|
|
2061
|
+
question: string,
|
|
2062
|
+
services: TDSServiceSchema[],
|
|
2063
|
+
coordinates: string,
|
|
2064
|
+
metadata: LegendAIProductMetadata,
|
|
2065
|
+
context: LegendAIOperationContext,
|
|
2066
|
+
startTime: number,
|
|
2067
|
+
orchestratorOptions?: LegendAIOrchestratorOptionsParam,
|
|
2068
|
+
): Promise<void> {
|
|
2069
|
+
const { config, setMessages } = context;
|
|
2070
|
+
const dataProductCoordinates = orchestratorOptions?.dataProductCoordinates;
|
|
2071
|
+
const hasOrchestratorFallback = Boolean(
|
|
2072
|
+
config.orchestratorUrl && dataProductCoordinates,
|
|
2073
|
+
);
|
|
2074
|
+
|
|
2075
|
+
if (services.length === 0) {
|
|
2076
|
+
handleSqlGenerationFailure(
|
|
2077
|
+
setMessages,
|
|
2078
|
+
startTime,
|
|
2079
|
+
hasOrchestratorFallback,
|
|
2080
|
+
'No TDS services available for SQL querying. You can try the Legend AI Orchestrator to generate a Pure query instead.',
|
|
2081
|
+
'No TDS services available for querying',
|
|
2082
|
+
LegendAIErrorType.GENERAL,
|
|
2083
|
+
);
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
addThinkingStep(setMessages, 'Found relevant services to query');
|
|
2088
|
+
|
|
2089
|
+
const selectedServices = await selectBestServices(
|
|
2090
|
+
question,
|
|
2091
|
+
services,
|
|
2092
|
+
context,
|
|
2093
|
+
);
|
|
2094
|
+
|
|
2095
|
+
const judgedSql = await generateAndJudgeSql(
|
|
2096
|
+
question,
|
|
2097
|
+
selectedServices,
|
|
2098
|
+
coordinates,
|
|
2099
|
+
context,
|
|
2100
|
+
startTime,
|
|
2101
|
+
metadata,
|
|
2102
|
+
);
|
|
2103
|
+
|
|
2104
|
+
if (!judgedSql) {
|
|
2105
|
+
addThinkingStep(
|
|
2106
|
+
setMessages,
|
|
2107
|
+
'SQL generation could not produce a valid query.',
|
|
2108
|
+
);
|
|
2109
|
+
handleSqlGenerationFailure(
|
|
2110
|
+
setMessages,
|
|
2111
|
+
startTime,
|
|
2112
|
+
hasOrchestratorFallback,
|
|
2113
|
+
SQL_GENERATION_FAILURE_WITH_ORCHESTRATOR,
|
|
2114
|
+
SQL_GENERATION_FAILURE_NO_ORCHESTRATOR,
|
|
2115
|
+
LegendAIErrorType.GENERATION,
|
|
2116
|
+
buildFallbackSuggestions(selectedServices),
|
|
2117
|
+
);
|
|
2118
|
+
return;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
// Check for any missing service parameters (date, id, key — anything)
|
|
2122
|
+
const missingParams = detectMissingServiceParams(judgedSql, selectedServices);
|
|
2123
|
+
if (missingParams.length > 0) {
|
|
2124
|
+
const sqlGenTimeValue = elapsedSeconds(startTime, 2);
|
|
2125
|
+
completeThinkingSteps(setMessages);
|
|
2126
|
+
addThinkingStep(
|
|
2127
|
+
setMessages,
|
|
2128
|
+
`Missing required parameter${missingParams.length > 1 ? 's' : ''}: ${missingParams.map((p) => p.name).join(', ')}`,
|
|
2129
|
+
);
|
|
2130
|
+
completeThinkingSteps(setMessages);
|
|
2131
|
+
updateLastAssistant(setMessages, () => ({
|
|
2132
|
+
sql: judgedSql,
|
|
2133
|
+
sqlGenTime: sqlGenTimeValue,
|
|
2134
|
+
textAnswer: buildMissingParamsWarning(missingParams),
|
|
2135
|
+
isProcessing: false,
|
|
2136
|
+
isExecuting: false,
|
|
2137
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
2138
|
+
}));
|
|
2139
|
+
return;
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
const sqlGenTimeValue = elapsedSeconds(startTime, 2);
|
|
2143
|
+
completeThinkingSteps(setMessages);
|
|
2144
|
+
updateLastAssistant(setMessages, () => ({
|
|
2145
|
+
sql: judgedSql,
|
|
2146
|
+
sqlGenTime: sqlGenTimeValue,
|
|
2147
|
+
isExecuting: true,
|
|
2148
|
+
}));
|
|
2149
|
+
|
|
2150
|
+
const execOutcome = await executeSqlWithRetries(
|
|
2151
|
+
judgedSql,
|
|
2152
|
+
question,
|
|
2153
|
+
selectedServices,
|
|
2154
|
+
coordinates,
|
|
2155
|
+
dataProductCoordinates,
|
|
2156
|
+
context,
|
|
2157
|
+
);
|
|
2158
|
+
|
|
2159
|
+
if (execOutcome.error) {
|
|
2160
|
+
const execErrorType = classifyError(new Error(execOutcome.error));
|
|
2161
|
+
addThinkingStep(
|
|
2162
|
+
setMessages,
|
|
2163
|
+
`Execution failed: ${execOutcome.error.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
2164
|
+
);
|
|
2165
|
+
finishWithThinkingError(
|
|
2166
|
+
setMessages,
|
|
2167
|
+
buildExecutionErrorMessage(execOutcome.error, selectedServices),
|
|
2168
|
+
startTime,
|
|
2169
|
+
execErrorType === LegendAIErrorType.GENERAL
|
|
2170
|
+
? LegendAIErrorType.EXECUTION
|
|
2171
|
+
: execErrorType,
|
|
2172
|
+
);
|
|
2173
|
+
updateLastAssistant(setMessages, () => ({
|
|
2174
|
+
isExecuting: false,
|
|
2175
|
+
suggestedQueries: buildFallbackSuggestions(selectedServices),
|
|
2176
|
+
...(hasOrchestratorFallback
|
|
2177
|
+
? {
|
|
2178
|
+
fallbackAction: {
|
|
2179
|
+
label: ORCHESTRATOR_FALLBACK_LABEL,
|
|
2180
|
+
actionId: LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
|
|
2181
|
+
},
|
|
2182
|
+
}
|
|
2183
|
+
: {}),
|
|
2184
|
+
}));
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
if (!execOutcome.result) {
|
|
2189
|
+
return;
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
let currentSql = execOutcome.sql;
|
|
2193
|
+
let sqlResult = execOutcome.result;
|
|
2194
|
+
|
|
2195
|
+
if (sqlResult.rows.length === 0) {
|
|
2196
|
+
const recovered = await tryRecoverZeroRows(
|
|
2197
|
+
currentSql,
|
|
2198
|
+
sqlResult,
|
|
2199
|
+
question,
|
|
2200
|
+
selectedServices,
|
|
2201
|
+
coordinates,
|
|
2202
|
+
dataProductCoordinates,
|
|
2203
|
+
context,
|
|
2204
|
+
);
|
|
2205
|
+
currentSql = recovered.sql;
|
|
2206
|
+
sqlResult = recovered.result;
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
await reportQueryResults(
|
|
2210
|
+
{
|
|
2211
|
+
currentSql,
|
|
2212
|
+
sqlResult,
|
|
2213
|
+
question,
|
|
2214
|
+
services: selectedServices,
|
|
2215
|
+
},
|
|
2216
|
+
metadata,
|
|
2217
|
+
context,
|
|
2218
|
+
startTime,
|
|
2219
|
+
hasOrchestratorFallback,
|
|
2220
|
+
);
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
function splitServicesByType(services: TDSServiceSchema[]): {
|
|
2224
|
+
tdsServices: TDSServiceSchema[];
|
|
2225
|
+
accessPoints: TDSServiceSchema[];
|
|
2226
|
+
} {
|
|
2227
|
+
return {
|
|
2228
|
+
tdsServices: services.filter(
|
|
2229
|
+
(s) => s.sourceType !== TDSServiceSourceType.ACCESS_POINT,
|
|
2230
|
+
),
|
|
2231
|
+
accessPoints: services.filter(
|
|
2232
|
+
(s) => s.sourceType === TDSServiceSourceType.ACCESS_POINT,
|
|
2233
|
+
),
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
async function routeToAccessPointOrData(
|
|
2238
|
+
question: string,
|
|
2239
|
+
services: TDSServiceSchema[],
|
|
2240
|
+
coordinates: string,
|
|
2241
|
+
metadata: LegendAIProductMetadata,
|
|
2242
|
+
context: LegendAIOperationContext,
|
|
2243
|
+
startTime: number,
|
|
2244
|
+
orchestratorOpts: LegendAIOrchestratorOptionsParam | undefined,
|
|
2245
|
+
): Promise<void> {
|
|
2246
|
+
const dataProductCoordinates = orchestratorOpts?.dataProductCoordinates;
|
|
2247
|
+
const { tdsServices, accessPoints } = splitServicesByType(services);
|
|
2248
|
+
if (
|
|
2249
|
+
tdsServices.length === 0 &&
|
|
2250
|
+
accessPoints.length > 0 &&
|
|
2251
|
+
dataProductCoordinates
|
|
2252
|
+
) {
|
|
2253
|
+
await processAccessPointQuery(
|
|
2254
|
+
question,
|
|
2255
|
+
accessPoints,
|
|
2256
|
+
metadata,
|
|
2257
|
+
context,
|
|
2258
|
+
startTime,
|
|
2259
|
+
dataProductCoordinates,
|
|
2260
|
+
);
|
|
2261
|
+
} else {
|
|
2262
|
+
await processDataQuery(
|
|
2263
|
+
question,
|
|
2264
|
+
tdsServices,
|
|
2265
|
+
coordinates,
|
|
2266
|
+
metadata,
|
|
2267
|
+
context,
|
|
2268
|
+
startTime,
|
|
2269
|
+
orchestratorOpts,
|
|
2270
|
+
);
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
async function handleAmbiguousIntent(
|
|
2275
|
+
question: string,
|
|
2276
|
+
services: TDSServiceSchema[],
|
|
2277
|
+
coordinates: string,
|
|
2278
|
+
metadata: LegendAIProductMetadata,
|
|
2279
|
+
context: LegendAIOperationContext,
|
|
2280
|
+
startTime: number,
|
|
2281
|
+
orchestratorOpts: LegendAIOrchestratorOptionsParam | undefined,
|
|
2282
|
+
): Promise<void> {
|
|
2283
|
+
const { setMessages } = context;
|
|
2284
|
+
addThinkingStep(
|
|
2285
|
+
setMessages,
|
|
2286
|
+
'Intent is ambiguous, providing metadata context and querying data...',
|
|
2287
|
+
);
|
|
2288
|
+
let metadataOverview: string | undefined;
|
|
2289
|
+
try {
|
|
2290
|
+
addThinkingStep(setMessages, 'Building metadata context...');
|
|
2291
|
+
metadataOverview = await buildMetadataOverview(question, metadata, context);
|
|
2292
|
+
} catch (metadataError) {
|
|
2293
|
+
assertErrorThrown(metadataError);
|
|
2294
|
+
addThinkingStep(
|
|
2295
|
+
setMessages,
|
|
2296
|
+
`Metadata context failed: ${metadataError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
2297
|
+
);
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
try {
|
|
2301
|
+
await routeToAccessPointOrData(
|
|
2302
|
+
question,
|
|
2303
|
+
services,
|
|
2304
|
+
coordinates,
|
|
2305
|
+
metadata,
|
|
2306
|
+
context,
|
|
2307
|
+
startTime,
|
|
2308
|
+
orchestratorOpts,
|
|
2309
|
+
);
|
|
2310
|
+
if (metadataOverview) {
|
|
2311
|
+
attachMetadataOverview(setMessages, metadataOverview);
|
|
2312
|
+
}
|
|
2313
|
+
} catch (queryError) {
|
|
2314
|
+
assertErrorThrown(queryError);
|
|
2315
|
+
addThinkingStep(
|
|
2316
|
+
setMessages,
|
|
2317
|
+
'Query failed, answering from product metadata...',
|
|
2318
|
+
);
|
|
2319
|
+
await handleMetadataQuestion(question, metadata, context, startTime, true);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
export async function processQuestion(
|
|
2324
|
+
question: string,
|
|
2325
|
+
services: TDSServiceSchema[],
|
|
2326
|
+
coordinates: string,
|
|
2327
|
+
metadata: LegendAIProductMetadata,
|
|
2328
|
+
context: LegendAIOperationContext,
|
|
2329
|
+
dataProductCoordinates?: LegendAIOrchestratorDataProductCoordinates,
|
|
2330
|
+
pureExecutionContext?: QueryExplicitExecutionContextInfo,
|
|
2331
|
+
): Promise<void> {
|
|
2332
|
+
const { config, plugin, setMessages } = context;
|
|
2333
|
+
const startTime = Date.now();
|
|
2334
|
+
|
|
2335
|
+
try {
|
|
2336
|
+
addThinkingStep(setMessages, 'Analyzing your question...');
|
|
2337
|
+
|
|
2338
|
+
const orchestratorOpts = dataProductCoordinates
|
|
2339
|
+
? {
|
|
2340
|
+
dataProductCoordinates,
|
|
2341
|
+
...(pureExecutionContext === undefined
|
|
2342
|
+
? {}
|
|
2343
|
+
: { pureExecutionContext }),
|
|
2344
|
+
}
|
|
2345
|
+
: undefined;
|
|
2346
|
+
|
|
2347
|
+
if (services.length === 0) {
|
|
2348
|
+
await handleMetadataQuestion(
|
|
2349
|
+
question,
|
|
2350
|
+
metadata,
|
|
2351
|
+
context,
|
|
2352
|
+
startTime,
|
|
2353
|
+
false,
|
|
2354
|
+
);
|
|
2355
|
+
if (config.orchestratorUrl && dataProductCoordinates) {
|
|
2356
|
+
updateLastAssistant(setMessages, () => ({
|
|
2357
|
+
fallbackAction: {
|
|
2358
|
+
label: ORCHESTRATOR_FALLBACK_LABEL,
|
|
2359
|
+
actionId: LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
|
|
2360
|
+
},
|
|
2361
|
+
}));
|
|
2362
|
+
}
|
|
2363
|
+
return;
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
const fastIntent = classifyQuestionIntentFast(question, true);
|
|
2367
|
+
if (
|
|
2368
|
+
fastIntent.intent === LegendAIQuestionIntent.METADATA &&
|
|
2369
|
+
!fastIntent.ambiguous
|
|
2370
|
+
) {
|
|
2371
|
+
await handleMetadataQuestion(
|
|
2372
|
+
question,
|
|
2373
|
+
metadata,
|
|
2374
|
+
context,
|
|
2375
|
+
startTime,
|
|
2376
|
+
true,
|
|
2377
|
+
);
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
if (fastIntent.ambiguous) {
|
|
2382
|
+
await handleAmbiguousIntent(
|
|
2383
|
+
question,
|
|
2384
|
+
services,
|
|
2385
|
+
coordinates,
|
|
2386
|
+
metadata,
|
|
2387
|
+
context,
|
|
2388
|
+
startTime,
|
|
2389
|
+
orchestratorOpts,
|
|
2390
|
+
);
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
const serviceNames = services.map((s) => s.title);
|
|
2395
|
+
const intent = await plugin.classifyQuestionIntent(
|
|
2396
|
+
question,
|
|
2397
|
+
true,
|
|
2398
|
+
config,
|
|
2399
|
+
serviceNames,
|
|
2400
|
+
);
|
|
2401
|
+
|
|
2402
|
+
if (intent === LegendAIQuestionIntent.METADATA) {
|
|
2403
|
+
await handleMetadataQuestion(
|
|
2404
|
+
question,
|
|
2405
|
+
metadata,
|
|
2406
|
+
context,
|
|
2407
|
+
startTime,
|
|
2408
|
+
true,
|
|
2409
|
+
);
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
try {
|
|
2414
|
+
await routeToAccessPointOrData(
|
|
2415
|
+
question,
|
|
2416
|
+
services,
|
|
2417
|
+
coordinates,
|
|
2418
|
+
metadata,
|
|
2419
|
+
context,
|
|
2420
|
+
startTime,
|
|
2421
|
+
orchestratorOpts,
|
|
2422
|
+
);
|
|
2423
|
+
} catch (sqlError) {
|
|
2424
|
+
assertErrorThrown(sqlError);
|
|
2425
|
+
addThinkingStep(
|
|
2426
|
+
setMessages,
|
|
2427
|
+
'SQL generation failed, answering from product metadata...',
|
|
2428
|
+
);
|
|
2429
|
+
await handleMetadataQuestion(
|
|
2430
|
+
question,
|
|
2431
|
+
metadata,
|
|
2432
|
+
context,
|
|
2433
|
+
startTime,
|
|
2434
|
+
true,
|
|
2435
|
+
);
|
|
2436
|
+
appendFallbackSuggestions(setMessages, services);
|
|
2437
|
+
}
|
|
2438
|
+
} catch (error) {
|
|
2439
|
+
assertErrorThrown(error);
|
|
2440
|
+
addThinkingStep(
|
|
2441
|
+
setMessages,
|
|
2442
|
+
`Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
2443
|
+
);
|
|
2444
|
+
finishWithThinkingError(
|
|
2445
|
+
setMessages,
|
|
2446
|
+
error.message,
|
|
2447
|
+
startTime,
|
|
2448
|
+
classifyError(error),
|
|
2449
|
+
);
|
|
2450
|
+
appendFallbackSuggestions(setMessages, services);
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
async function routeDataQueryWithErrorHandling(
|
|
2455
|
+
question: string,
|
|
2456
|
+
services: TDSServiceSchema[],
|
|
2457
|
+
coordinates: string,
|
|
2458
|
+
metadata: LegendAIProductMetadata,
|
|
2459
|
+
context: LegendAIOperationContext,
|
|
2460
|
+
orchestratorOptions: LegendAIOrchestratorOptionsParam | undefined,
|
|
2461
|
+
): Promise<void> {
|
|
2462
|
+
const { setMessages } = context;
|
|
2463
|
+
const startTime = Date.now();
|
|
2464
|
+
try {
|
|
2465
|
+
addThinkingStep(setMessages, 'Preparing data query...');
|
|
2466
|
+
await routeToAccessPointOrData(
|
|
2467
|
+
question,
|
|
2468
|
+
services,
|
|
2469
|
+
coordinates,
|
|
2470
|
+
metadata,
|
|
2471
|
+
context,
|
|
2472
|
+
startTime,
|
|
2473
|
+
orchestratorOptions,
|
|
2474
|
+
);
|
|
2475
|
+
} catch (error) {
|
|
2476
|
+
assertErrorThrown(error);
|
|
2477
|
+
addThinkingStep(
|
|
2478
|
+
setMessages,
|
|
2479
|
+
`Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
2480
|
+
);
|
|
2481
|
+
finishWithThinkingError(
|
|
2482
|
+
setMessages,
|
|
2483
|
+
error.message,
|
|
2484
|
+
startTime,
|
|
2485
|
+
classifyError(error),
|
|
2486
|
+
);
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
export async function processQuestionWithIntent(
|
|
2491
|
+
question: string,
|
|
2492
|
+
intent: LegendAIQuestionIntent,
|
|
2493
|
+
services: TDSServiceSchema[],
|
|
2494
|
+
coordinates: string,
|
|
2495
|
+
metadata: LegendAIProductMetadata,
|
|
2496
|
+
context: LegendAIOperationContext,
|
|
2497
|
+
orchestratorOptions?: LegendAIOrchestratorOptionsParam,
|
|
2498
|
+
): Promise<void> {
|
|
2499
|
+
const { config, setMessages } = context;
|
|
2500
|
+
const dataProductCoordinates = orchestratorOptions?.dataProductCoordinates;
|
|
2501
|
+
|
|
2502
|
+
if (intent === LegendAIQuestionIntent.METADATA) {
|
|
2503
|
+
const startTime = Date.now();
|
|
2504
|
+
try {
|
|
2505
|
+
await handleMetadataQuestion(
|
|
2506
|
+
question,
|
|
2507
|
+
metadata,
|
|
2508
|
+
context,
|
|
2509
|
+
startTime,
|
|
2510
|
+
services.length > 0,
|
|
2511
|
+
);
|
|
2512
|
+
} catch (error) {
|
|
2513
|
+
assertErrorThrown(error);
|
|
2514
|
+
addThinkingStep(
|
|
2515
|
+
setMessages,
|
|
2516
|
+
`Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
2517
|
+
);
|
|
2518
|
+
finishWithThinkingError(
|
|
2519
|
+
setMessages,
|
|
2520
|
+
error.message,
|
|
2521
|
+
startTime,
|
|
2522
|
+
classifyError(error),
|
|
2523
|
+
);
|
|
2524
|
+
}
|
|
2525
|
+
return;
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
if (
|
|
2529
|
+
intent === LegendAIQuestionIntent.ORCHESTRATOR &&
|
|
2530
|
+
config.orchestratorUrl &&
|
|
2531
|
+
dataProductCoordinates
|
|
2532
|
+
) {
|
|
2533
|
+
if (services.length > 0) {
|
|
2534
|
+
await routeDataQueryWithErrorHandling(
|
|
2535
|
+
question,
|
|
2536
|
+
services,
|
|
2537
|
+
coordinates,
|
|
2538
|
+
metadata,
|
|
2539
|
+
context,
|
|
2540
|
+
orchestratorOptions,
|
|
2541
|
+
);
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
const startTime = Date.now();
|
|
2545
|
+
await handleMetadataQuestion(question, metadata, context, startTime, false);
|
|
2546
|
+
updateLastAssistant(setMessages, () => ({
|
|
2547
|
+
fallbackAction: {
|
|
2548
|
+
label: ORCHESTRATOR_FALLBACK_LABEL,
|
|
2549
|
+
actionId: LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
|
|
2550
|
+
},
|
|
2551
|
+
}));
|
|
2552
|
+
return;
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
await routeDataQueryWithErrorHandling(
|
|
2556
|
+
question,
|
|
2557
|
+
services,
|
|
2558
|
+
coordinates,
|
|
2559
|
+
metadata,
|
|
2560
|
+
context,
|
|
2561
|
+
orchestratorOptions,
|
|
2562
|
+
);
|
|
2563
|
+
}
|