@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
|
@@ -14,1683 +14,31 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
18
|
-
import {
|
|
17
|
+
import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
|
18
|
+
import { noop } from '@finos/legend-shared';
|
|
19
19
|
import {
|
|
20
20
|
type TDSServiceSchema,
|
|
21
21
|
type LegendAIConfig,
|
|
22
22
|
type LegendAIChatState,
|
|
23
|
-
type LegendAIAssistantMessage,
|
|
24
|
-
type LegendAIUserMessage,
|
|
25
23
|
type LegendAIMessage,
|
|
26
24
|
type LegendAIConversationTurn,
|
|
27
25
|
type LegendAIProductMetadata,
|
|
28
|
-
type
|
|
29
|
-
LegendAIQuestionIntent,
|
|
30
|
-
LegendAIThinkingStepStatus,
|
|
26
|
+
type LegendAIScopeItem,
|
|
27
|
+
type LegendAIQuestionIntent,
|
|
31
28
|
LegendAIMessageRole,
|
|
32
|
-
LegendAIErrorType,
|
|
33
|
-
LegendAIServiceError,
|
|
34
|
-
TDSServiceSourceType,
|
|
35
|
-
buildColumnDefsFromNames,
|
|
36
|
-
LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
|
|
37
29
|
} from '../LegendAITypes.js';
|
|
38
30
|
import {
|
|
39
31
|
type LegendAI_LegendApplicationPlugin_Extension,
|
|
40
32
|
type LegendAIOrchestratorDataProductCoordinates,
|
|
41
|
-
type LegendAISqlExecutionResultData,
|
|
42
|
-
type LegendAIResolvedEntities,
|
|
43
|
-
LegendAIJudgeVerdict,
|
|
44
33
|
} from '../LegendAI_LegendApplicationPlugin_Extension.js';
|
|
45
34
|
import type { QueryExplicitExecutionContextInfo } from '@finos/legend-graph';
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const SUGGESTED_QUERIES_DELIMITER = '---SUGGESTED_QUERIES---';
|
|
54
|
-
|
|
55
|
-
export function elapsedSeconds(startTime: number, decimals: 1 | 2 = 1): string {
|
|
56
|
-
return ((Date.now() - startTime) / 1000).toFixed(decimals);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function withTimeout<T>(
|
|
60
|
-
promise: Promise<T>,
|
|
61
|
-
ms: number,
|
|
62
|
-
): Promise<T | undefined> {
|
|
63
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
64
|
-
return Promise.race([
|
|
65
|
-
promise.finally(() => {
|
|
66
|
-
if (timer !== undefined) {
|
|
67
|
-
clearTimeout(timer);
|
|
68
|
-
}
|
|
69
|
-
}),
|
|
70
|
-
new Promise<undefined>((resolve) => {
|
|
71
|
-
timer = setTimeout(() => resolve(undefined), ms);
|
|
72
|
-
}),
|
|
73
|
-
]);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function deduplicateColumns(columns: string[]): string[] {
|
|
77
|
-
const seen = new Map<string, number>();
|
|
78
|
-
return columns.map((col) => {
|
|
79
|
-
const count = seen.get(col) ?? 0;
|
|
80
|
-
seen.set(col, count + 1);
|
|
81
|
-
return count === 0 ? col : `${col}_${count + 1}`;
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export type MessageSetter = React.Dispatch<
|
|
86
|
-
React.SetStateAction<LegendAIMessage[]>
|
|
87
|
-
>;
|
|
88
|
-
|
|
89
|
-
export function createMessagePair(
|
|
90
|
-
text: string,
|
|
91
|
-
): [LegendAIUserMessage, LegendAIAssistantMessage] {
|
|
92
|
-
return [
|
|
93
|
-
{ id: uuid(), role: LegendAIMessageRole.USER, text },
|
|
94
|
-
{
|
|
95
|
-
id: uuid(),
|
|
96
|
-
role: LegendAIMessageRole.ASSISTANT,
|
|
97
|
-
thinkingSteps: [],
|
|
98
|
-
sql: null,
|
|
99
|
-
textAnswer: null,
|
|
100
|
-
dataContext: null,
|
|
101
|
-
gridData: null,
|
|
102
|
-
error: null,
|
|
103
|
-
errorType: null,
|
|
104
|
-
sqlGenTime: null,
|
|
105
|
-
execTime: null,
|
|
106
|
-
thinkingDuration: null,
|
|
107
|
-
isProcessing: true,
|
|
108
|
-
isExecuting: false,
|
|
109
|
-
suggestedQueries: [],
|
|
110
|
-
fallbackAction: null,
|
|
111
|
-
},
|
|
112
|
-
];
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export interface LegendAIOperationContext {
|
|
116
|
-
config: LegendAIConfig;
|
|
117
|
-
plugin: LegendAI_LegendApplicationPlugin_Extension;
|
|
118
|
-
history: LegendAIConversationTurn[];
|
|
119
|
-
setMessages: MessageSetter;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
interface LegendAIOrchestratorOptionsParam {
|
|
123
|
-
dataProductCoordinates: LegendAIOrchestratorDataProductCoordinates;
|
|
124
|
-
pureExecutionContext?: QueryExplicitExecutionContextInfo;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function updateLastAssistant(
|
|
128
|
-
setMessages: MessageSetter,
|
|
129
|
-
updater: (msg: LegendAIAssistantMessage) => Partial<LegendAIAssistantMessage>,
|
|
130
|
-
): void {
|
|
131
|
-
setMessages((prev) => {
|
|
132
|
-
const newMsgs = [...prev];
|
|
133
|
-
const lastIdx = newMsgs.length - 1;
|
|
134
|
-
const last = newMsgs[lastIdx];
|
|
135
|
-
if (last?.role === LegendAIMessageRole.ASSISTANT) {
|
|
136
|
-
newMsgs[lastIdx] = { ...last, ...updater(last) };
|
|
137
|
-
}
|
|
138
|
-
return newMsgs;
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function addThinkingStep(
|
|
143
|
-
setMessages: MessageSetter,
|
|
144
|
-
label: string,
|
|
145
|
-
): void {
|
|
146
|
-
updateLastAssistant(setMessages, (msg) => ({
|
|
147
|
-
thinkingSteps: [
|
|
148
|
-
...msg.thinkingSteps.map((s) =>
|
|
149
|
-
s.status === LegendAIThinkingStepStatus.ACTIVE
|
|
150
|
-
? { ...s, status: LegendAIThinkingStepStatus.DONE }
|
|
151
|
-
: s,
|
|
152
|
-
),
|
|
153
|
-
{ id: uuid(), label, status: LegendAIThinkingStepStatus.ACTIVE },
|
|
154
|
-
],
|
|
155
|
-
}));
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export function completeThinkingSteps(setMessages: MessageSetter): void {
|
|
159
|
-
updateLastAssistant(setMessages, (msg) => ({
|
|
160
|
-
thinkingSteps: msg.thinkingSteps.map((s) =>
|
|
161
|
-
s.status === LegendAIThinkingStepStatus.ACTIVE
|
|
162
|
-
? { ...s, status: LegendAIThinkingStepStatus.DONE }
|
|
163
|
-
: s,
|
|
164
|
-
),
|
|
165
|
-
}));
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export function classifyError(error: Error): LegendAIErrorType {
|
|
169
|
-
if (error instanceof LegendAIServiceError) {
|
|
170
|
-
return error.errorType;
|
|
171
|
-
}
|
|
172
|
-
return LegendAIErrorType.GENERAL;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export function finishWithThinkingError(
|
|
176
|
-
setMessages: MessageSetter,
|
|
177
|
-
errorMsg: string,
|
|
178
|
-
startTime: number,
|
|
179
|
-
errorType?: LegendAIErrorType,
|
|
180
|
-
): void {
|
|
181
|
-
updateLastAssistant(setMessages, (msg) => ({
|
|
182
|
-
thinkingSteps: msg.thinkingSteps.map((s) =>
|
|
183
|
-
s.status === LegendAIThinkingStepStatus.ACTIVE
|
|
184
|
-
? { ...s, status: LegendAIThinkingStepStatus.ERROR }
|
|
185
|
-
: s,
|
|
186
|
-
),
|
|
187
|
-
error: errorMsg.slice(0, MAX_ERROR_MESSAGE_LENGTH),
|
|
188
|
-
errorType: errorType ?? null,
|
|
189
|
-
isProcessing: false,
|
|
190
|
-
thinkingDuration: elapsedSeconds(startTime),
|
|
191
|
-
}));
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export function buildConversationHistory(
|
|
195
|
-
messages: LegendAIMessage[],
|
|
196
|
-
): LegendAIConversationTurn[] {
|
|
197
|
-
const history: LegendAIConversationTurn[] = [];
|
|
198
|
-
let i = 0;
|
|
199
|
-
while (i < messages.length - 1) {
|
|
200
|
-
const userMsg = messages[i];
|
|
201
|
-
const asstMsg = messages[i + 1];
|
|
202
|
-
if (
|
|
203
|
-
userMsg?.role === LegendAIMessageRole.USER &&
|
|
204
|
-
asstMsg?.role === LegendAIMessageRole.ASSISTANT
|
|
205
|
-
) {
|
|
206
|
-
if (asstMsg.sql) {
|
|
207
|
-
history.push({
|
|
208
|
-
question: userMsg.text,
|
|
209
|
-
sql: asstMsg.sql,
|
|
210
|
-
intent: LegendAIQuestionIntent.DATA_QUERY,
|
|
211
|
-
});
|
|
212
|
-
} else if (asstMsg.textAnswer) {
|
|
213
|
-
history.push({
|
|
214
|
-
question: userMsg.text,
|
|
215
|
-
sql: asstMsg.textAnswer,
|
|
216
|
-
intent: LegendAIQuestionIntent.METADATA,
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
i += 2;
|
|
220
|
-
} else {
|
|
221
|
-
i += 1;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
return history;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function formatServiceParams(services: TDSServiceSchema[]): string[] {
|
|
228
|
-
return services.flatMap((s) =>
|
|
229
|
-
s.parameters.length > 0 ? [`${s.title}: ${s.parameters.join(', ')}`] : [],
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
export function buildGenerationFailureMessage(
|
|
234
|
-
failure: string,
|
|
235
|
-
suggestion: string | undefined,
|
|
236
|
-
services: TDSServiceSchema[],
|
|
237
|
-
): string {
|
|
238
|
-
const parts = [failure];
|
|
239
|
-
if (suggestion) {
|
|
240
|
-
parts.push(`\nTry instead: "${suggestion}"`);
|
|
241
|
-
}
|
|
242
|
-
const svcNames = services.map((s) => s.title);
|
|
243
|
-
if (svcNames.length > 0) {
|
|
244
|
-
parts.push(`\nAvailable services: ${svcNames.join(', ')}`);
|
|
245
|
-
}
|
|
246
|
-
const allParams = formatServiceParams(services);
|
|
247
|
-
if (allParams.length > 0) {
|
|
248
|
-
parts.push(`\nService parameters: ${allParams.join('; ')}`);
|
|
249
|
-
}
|
|
250
|
-
return parts.join('');
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
export function buildExecutionErrorMessage(
|
|
254
|
-
errStr: string,
|
|
255
|
-
services: TDSServiceSchema[],
|
|
256
|
-
): string {
|
|
257
|
-
const errParts: string[] = [];
|
|
258
|
-
const errLower = errStr.toLowerCase();
|
|
259
|
-
|
|
260
|
-
const missingParamMatch =
|
|
261
|
-
/missing required parameter values?\s*\[(?<params>[^\]]+)\]/i.exec(errStr);
|
|
262
|
-
if (missingParamMatch) {
|
|
263
|
-
const paramNames = missingParamMatch.groups?.params ?? '';
|
|
264
|
-
const paramList = paramNames.split(',').map((p) => p.trim());
|
|
265
|
-
const hint = paramList
|
|
266
|
-
.map(
|
|
267
|
-
(p) =>
|
|
268
|
-
`a specific ${p.replaceAll(/(?<lower>[a-z])(?<upper>[A-Z])/g, '$<lower> $<upper>').toLowerCase()}`,
|
|
269
|
-
)
|
|
270
|
-
.join(' and ');
|
|
271
|
-
errParts.push(
|
|
272
|
-
`This service requires a value for: ${paramNames}`,
|
|
273
|
-
`\nTry rephrasing your question to include ${hint}.`,
|
|
274
|
-
);
|
|
275
|
-
const svcParams = formatServiceParams(services);
|
|
276
|
-
if (svcParams.length > 0) {
|
|
277
|
-
errParts.push(`\nService parameters:\n${svcParams.join('\n')}`);
|
|
278
|
-
}
|
|
279
|
-
return errParts.join('');
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
errParts.push(errStr.slice(0, MAX_ERROR_MESSAGE_LENGTH));
|
|
283
|
-
if (
|
|
284
|
-
errLower.includes('column') &&
|
|
285
|
-
(errLower.includes('not found') ||
|
|
286
|
-
errLower.includes('does not exist') ||
|
|
287
|
-
errLower.includes('unknown'))
|
|
288
|
-
) {
|
|
289
|
-
const svcCols = services.map(
|
|
290
|
-
(s) => `${s.title}: ${s.columns.map((c) => c.name).join(', ')}`,
|
|
291
|
-
);
|
|
292
|
-
errParts.push(`\nAvailable columns:\n${svcCols.join('\n')}`);
|
|
293
|
-
}
|
|
294
|
-
if (errLower.includes('parameter') || errLower.includes('argument')) {
|
|
295
|
-
const svcParams = formatServiceParams(services);
|
|
296
|
-
if (svcParams.length > 0) {
|
|
297
|
-
errParts.push(`\nRequired parameters:\n${svcParams.join('\n')}`);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
return errParts.join('');
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function parseSuggestedQueries(rawAnswer: string): {
|
|
304
|
-
answer: string;
|
|
305
|
-
suggestedQueries: string[];
|
|
306
|
-
} {
|
|
307
|
-
const delimIndex = rawAnswer.indexOf(SUGGESTED_QUERIES_DELIMITER);
|
|
308
|
-
if (delimIndex === -1) {
|
|
309
|
-
return { answer: rawAnswer.trim(), suggestedQueries: [] };
|
|
310
|
-
}
|
|
311
|
-
const answer = rawAnswer.slice(0, delimIndex).trim();
|
|
312
|
-
const suggestionsBlock = rawAnswer.slice(
|
|
313
|
-
delimIndex + SUGGESTED_QUERIES_DELIMITER.length,
|
|
314
|
-
);
|
|
315
|
-
const suggestedQueries = suggestionsBlock
|
|
316
|
-
.split('\n')
|
|
317
|
-
.map((line) => line.replace(/^\d+[.)]\s*/, '').trim())
|
|
318
|
-
.filter((line) => line.length > 0)
|
|
319
|
-
.slice(0, 3);
|
|
320
|
-
return { answer, suggestedQueries };
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
export async function handleMetadataQuestion(
|
|
324
|
-
question: string,
|
|
325
|
-
metadata: LegendAIProductMetadata,
|
|
326
|
-
context: LegendAIOperationContext,
|
|
327
|
-
startTime: number,
|
|
328
|
-
hasQueryableServices?: boolean,
|
|
329
|
-
): Promise<void> {
|
|
330
|
-
const { config, plugin, history, setMessages } = context;
|
|
331
|
-
addThinkingStep(setMessages, 'Answering from product metadata...');
|
|
332
|
-
const metadataPromptText = plugin.buildMetadataPrompt(
|
|
333
|
-
question,
|
|
334
|
-
metadata,
|
|
335
|
-
history,
|
|
336
|
-
);
|
|
337
|
-
const rawAnswer = await plugin.callLLM(metadataPromptText, config);
|
|
338
|
-
const { answer, suggestedQueries: parsedSuggestions } =
|
|
339
|
-
parseSuggestedQueries(rawAnswer);
|
|
340
|
-
const suggestedQueries =
|
|
341
|
-
hasQueryableServices === false && !config.orchestratorUrl
|
|
342
|
-
? []
|
|
343
|
-
: parsedSuggestions;
|
|
344
|
-
completeThinkingSteps(setMessages);
|
|
345
|
-
updateLastAssistant(setMessages, () => ({
|
|
346
|
-
textAnswer: answer,
|
|
347
|
-
suggestedQueries,
|
|
348
|
-
isProcessing: false,
|
|
349
|
-
thinkingDuration: elapsedSeconds(startTime),
|
|
350
|
-
}));
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
export async function generateAndJudgeSql(
|
|
354
|
-
question: string,
|
|
355
|
-
services: TDSServiceSchema[],
|
|
356
|
-
coordinates: string,
|
|
357
|
-
context: LegendAIOperationContext,
|
|
358
|
-
startTime: number,
|
|
359
|
-
): Promise<string | null> {
|
|
360
|
-
const { config, plugin, history, setMessages } = context;
|
|
361
|
-
addThinkingStep(setMessages, 'Building context from service schemas...');
|
|
362
|
-
const prompt = plugin.buildGeneratorPrompt(
|
|
363
|
-
question,
|
|
364
|
-
services,
|
|
365
|
-
coordinates,
|
|
366
|
-
history,
|
|
367
|
-
);
|
|
368
|
-
addThinkingStep(setMessages, 'Generating SQL query...');
|
|
369
|
-
|
|
370
|
-
const answerText = await plugin.callLLM(prompt, config);
|
|
371
|
-
const {
|
|
372
|
-
sql: generatedSql,
|
|
373
|
-
failure,
|
|
374
|
-
suggestion,
|
|
375
|
-
} = plugin.extractSqlFromResponse(answerText);
|
|
376
|
-
|
|
377
|
-
if (failure) {
|
|
378
|
-
addThinkingStep(setMessages, `Generation failed: ${failure}`);
|
|
379
|
-
finishWithThinkingError(
|
|
380
|
-
setMessages,
|
|
381
|
-
buildGenerationFailureMessage(failure, suggestion, services),
|
|
382
|
-
startTime,
|
|
383
|
-
LegendAIErrorType.GENERATION,
|
|
384
|
-
);
|
|
385
|
-
return null;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (!generatedSql) {
|
|
389
|
-
addThinkingStep(setMessages, 'Could not extract SQL from response');
|
|
390
|
-
finishWithThinkingError(
|
|
391
|
-
setMessages,
|
|
392
|
-
'Could not extract SQL from LLM response.\nTry rephrasing your question or ask about a specific service.',
|
|
393
|
-
startTime,
|
|
394
|
-
LegendAIErrorType.GENERATION,
|
|
395
|
-
);
|
|
396
|
-
return null;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
const maxAttempts = config.maxJudgeAttempts ?? DEFAULT_MAX_JUDGE_ATTEMPTS;
|
|
400
|
-
let currentSql = generatedSql;
|
|
401
|
-
|
|
402
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
403
|
-
addThinkingStep(
|
|
404
|
-
setMessages,
|
|
405
|
-
`Verifying query correctness (${attempt}/${maxAttempts})...`,
|
|
406
|
-
);
|
|
407
|
-
|
|
408
|
-
const judgePrompt = plugin.buildJudgePrompt(
|
|
409
|
-
currentSql,
|
|
410
|
-
question,
|
|
411
|
-
services,
|
|
412
|
-
coordinates,
|
|
413
|
-
history,
|
|
414
|
-
);
|
|
415
|
-
const judgeAnswer = await plugin.callLLM(judgePrompt, config);
|
|
416
|
-
const judgeResult = plugin.extractJudgeResult(judgeAnswer);
|
|
417
|
-
|
|
418
|
-
if (judgeResult.verdict === LegendAIJudgeVerdict.PASS) {
|
|
419
|
-
completeThinkingSteps(setMessages);
|
|
420
|
-
return currentSql;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
const previousSql = currentSql;
|
|
424
|
-
const correctedSql = judgeResult.correctedSql?.trim();
|
|
425
|
-
if (
|
|
426
|
-
correctedSql !== undefined &&
|
|
427
|
-
(correctedSql.toLowerCase().startsWith('select') ||
|
|
428
|
-
correctedSql.toLowerCase().startsWith('with') ||
|
|
429
|
-
correctedSql.toLowerCase().startsWith('('))
|
|
430
|
-
) {
|
|
431
|
-
addThinkingStep(setMessages, `Query corrected (attempt ${attempt})`);
|
|
432
|
-
currentSql = correctedSql;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
if (currentSql === previousSql || attempt === maxAttempts) {
|
|
436
|
-
addThinkingStep(
|
|
437
|
-
setMessages,
|
|
438
|
-
'Max verification attempts reached, using best query',
|
|
439
|
-
);
|
|
440
|
-
return currentSql;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
return null;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function reportExecutionResult(
|
|
448
|
-
rawResult: LegendAISqlExecutionResultData,
|
|
449
|
-
setMessages: MessageSetter,
|
|
450
|
-
execStartTime: number,
|
|
451
|
-
startTime: number,
|
|
452
|
-
): LegendAISqlExecutionResultData {
|
|
453
|
-
const columns = deduplicateColumns(rawResult.columns);
|
|
454
|
-
const rows = rawResult.rows;
|
|
455
|
-
completeThinkingSteps(setMessages);
|
|
456
|
-
addThinkingStep(
|
|
457
|
-
setMessages,
|
|
458
|
-
`Retrieved ${rows.length} row${rows.length === 1 ? '' : 's'}`,
|
|
459
|
-
);
|
|
460
|
-
completeThinkingSteps(setMessages);
|
|
461
|
-
|
|
462
|
-
updateLastAssistant(setMessages, () => ({
|
|
463
|
-
gridData: { columnDefs: buildColumnDefsFromNames(columns), rowData: rows },
|
|
464
|
-
execTime: elapsedSeconds(execStartTime, 2),
|
|
465
|
-
isProcessing: false,
|
|
466
|
-
isExecuting: false,
|
|
467
|
-
thinkingDuration: elapsedSeconds(startTime),
|
|
468
|
-
}));
|
|
469
|
-
return { columns, rows };
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
export async function executeSqlAndReport(
|
|
473
|
-
sql: string,
|
|
474
|
-
services: TDSServiceSchema[],
|
|
475
|
-
config: LegendAIConfig,
|
|
476
|
-
plugin: LegendAI_LegendApplicationPlugin_Extension,
|
|
477
|
-
setMessages: MessageSetter,
|
|
478
|
-
startTime: number,
|
|
479
|
-
dataProductCoordinates?: LegendAIOrchestratorDataProductCoordinates,
|
|
480
|
-
): Promise<LegendAISqlExecutionResultData | undefined> {
|
|
481
|
-
const execStartTime = Date.now();
|
|
482
|
-
try {
|
|
483
|
-
const isAccessPoint = services.some(
|
|
484
|
-
(s) => s.sourceType === TDSServiceSourceType.ACCESS_POINT,
|
|
485
|
-
);
|
|
486
|
-
const rawResult =
|
|
487
|
-
isAccessPoint && dataProductCoordinates
|
|
488
|
-
? await plugin.executeLakehouseSql(sql, dataProductCoordinates, config)
|
|
489
|
-
: await plugin.executeSql(sql, config);
|
|
490
|
-
return reportExecutionResult(
|
|
491
|
-
rawResult,
|
|
492
|
-
setMessages,
|
|
493
|
-
execStartTime,
|
|
494
|
-
startTime,
|
|
495
|
-
);
|
|
496
|
-
} catch (executeError) {
|
|
497
|
-
assertErrorThrown(executeError);
|
|
498
|
-
const execErrorType = classifyError(executeError);
|
|
499
|
-
addThinkingStep(
|
|
500
|
-
setMessages,
|
|
501
|
-
`Execution failed: ${executeError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
502
|
-
);
|
|
503
|
-
finishWithThinkingError(
|
|
504
|
-
setMessages,
|
|
505
|
-
buildExecutionErrorMessage(executeError.message, services),
|
|
506
|
-
startTime,
|
|
507
|
-
execErrorType === LegendAIErrorType.GENERAL
|
|
508
|
-
? LegendAIErrorType.EXECUTION
|
|
509
|
-
: execErrorType,
|
|
510
|
-
);
|
|
511
|
-
updateLastAssistant(setMessages, () => ({
|
|
512
|
-
execTime: elapsedSeconds(execStartTime, 2),
|
|
513
|
-
isExecuting: false,
|
|
514
|
-
}));
|
|
515
|
-
return undefined;
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
export async function executePureQueryAndReport(
|
|
520
|
-
pureQuery: string,
|
|
521
|
-
pureExecutionContext: QueryExplicitExecutionContextInfo,
|
|
522
|
-
dataProductCoordinates: LegendAIOrchestratorDataProductCoordinates,
|
|
523
|
-
config: LegendAIConfig,
|
|
524
|
-
plugin: LegendAI_LegendApplicationPlugin_Extension,
|
|
525
|
-
setMessages: MessageSetter,
|
|
526
|
-
startTime: number,
|
|
527
|
-
): Promise<LegendAISqlExecutionResultData> {
|
|
528
|
-
const execStartTime = Date.now();
|
|
529
|
-
try {
|
|
530
|
-
addThinkingStep(setMessages, 'Executing Pure query...');
|
|
531
|
-
const rawResult = await plugin.executePureQuery(
|
|
532
|
-
pureQuery,
|
|
533
|
-
pureExecutionContext,
|
|
534
|
-
dataProductCoordinates,
|
|
535
|
-
config,
|
|
536
|
-
);
|
|
537
|
-
return reportExecutionResult(
|
|
538
|
-
rawResult,
|
|
539
|
-
setMessages,
|
|
540
|
-
execStartTime,
|
|
541
|
-
startTime,
|
|
542
|
-
);
|
|
543
|
-
} catch (executeError) {
|
|
544
|
-
assertErrorThrown(executeError);
|
|
545
|
-
const execErrorType = classifyError(executeError);
|
|
546
|
-
addThinkingStep(
|
|
547
|
-
setMessages,
|
|
548
|
-
`Execution failed: ${executeError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
549
|
-
);
|
|
550
|
-
completeThinkingSteps(setMessages);
|
|
551
|
-
updateLastAssistant(setMessages, () => ({
|
|
552
|
-
execTime: elapsedSeconds(execStartTime, 2),
|
|
553
|
-
isExecuting: false,
|
|
554
|
-
isProcessing: false,
|
|
555
|
-
error: `Execution failed: ${executeError.message.slice(0, MAX_ERROR_MESSAGE_LENGTH)}`,
|
|
556
|
-
errorType:
|
|
557
|
-
execErrorType === LegendAIErrorType.GENERAL
|
|
558
|
-
? LegendAIErrorType.EXECUTION
|
|
559
|
-
: execErrorType,
|
|
560
|
-
thinkingDuration: elapsedSeconds(startTime),
|
|
561
|
-
}));
|
|
562
|
-
return { columns: [], rows: [] };
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
export async function analyzeOrchestratorResults(
|
|
567
|
-
question: string,
|
|
568
|
-
query: string,
|
|
569
|
-
execResult: LegendAISqlExecutionResultData,
|
|
570
|
-
metadata: LegendAIProductMetadata,
|
|
571
|
-
context: LegendAIOperationContext,
|
|
572
|
-
startTime: number,
|
|
573
|
-
): Promise<void> {
|
|
574
|
-
const { config, plugin, setMessages } = context;
|
|
575
|
-
addThinkingStep(setMessages, 'Analyzing results...');
|
|
576
|
-
updateLastAssistant(setMessages, () => ({
|
|
577
|
-
isProcessing: true,
|
|
578
|
-
}));
|
|
579
|
-
const analysis = await withTimeout(
|
|
580
|
-
plugin.analyzeQueryResults(
|
|
581
|
-
question,
|
|
582
|
-
query,
|
|
583
|
-
execResult.columns,
|
|
584
|
-
execResult.rows,
|
|
585
|
-
metadata,
|
|
586
|
-
config,
|
|
587
|
-
),
|
|
588
|
-
ANALYSIS_TIMEOUT_MS,
|
|
589
|
-
);
|
|
590
|
-
if (analysis) {
|
|
591
|
-
completeThinkingSteps(setMessages);
|
|
592
|
-
updateLastAssistant(setMessages, () => ({
|
|
593
|
-
textAnswer: analysis.summary,
|
|
594
|
-
suggestedQueries: analysis.suggestedQueries,
|
|
595
|
-
isProcessing: false,
|
|
596
|
-
thinkingDuration: elapsedSeconds(startTime),
|
|
597
|
-
}));
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
async function handleEmptyOrchestratorResults(
|
|
602
|
-
question: string,
|
|
603
|
-
legendQuery: string,
|
|
604
|
-
orchestratorOptions: Required<LegendAIOrchestratorOptionsParam>,
|
|
605
|
-
metadata: LegendAIProductMetadata,
|
|
606
|
-
resolvedEntities: LegendAIResolvedEntities,
|
|
607
|
-
context: LegendAIOperationContext,
|
|
608
|
-
startTime: number,
|
|
609
|
-
): Promise<void> {
|
|
610
|
-
const { dataProductCoordinates, pureExecutionContext } = orchestratorOptions;
|
|
611
|
-
const { config, plugin, setMessages } = context;
|
|
612
|
-
|
|
613
|
-
if (resolvedEntities.relatedEntities.length > 0) {
|
|
614
|
-
const alternateRoot = resolvedEntities.relatedEntities[0];
|
|
615
|
-
if (alternateRoot) {
|
|
616
|
-
addThinkingStep(
|
|
617
|
-
setMessages,
|
|
618
|
-
`No results with ${resolvedEntities.rootEntity.split('::').pop() ?? resolvedEntities.rootEntity}, retrying with ${alternateRoot.split('::').pop() ?? alternateRoot}...`,
|
|
619
|
-
);
|
|
620
|
-
|
|
621
|
-
try {
|
|
622
|
-
const retryResponse = await plugin.generateQueryViaOrchestrator(
|
|
623
|
-
{
|
|
624
|
-
user_question: question,
|
|
625
|
-
semantic_search_resolution_details: {
|
|
626
|
-
data_product_coordinates: dataProductCoordinates,
|
|
627
|
-
root_entity: alternateRoot,
|
|
628
|
-
related_entities: resolvedEntities.relatedEntities.slice(1),
|
|
629
|
-
},
|
|
630
|
-
},
|
|
631
|
-
config,
|
|
632
|
-
);
|
|
633
|
-
|
|
634
|
-
updateLastAssistant(setMessages, () => ({
|
|
635
|
-
sql: retryResponse.legend_query,
|
|
636
|
-
sqlGenTime: elapsedSeconds(startTime, 2),
|
|
637
|
-
isExecuting: true,
|
|
638
|
-
}));
|
|
639
|
-
|
|
640
|
-
const retryResult = await executePureQueryAndReport(
|
|
641
|
-
retryResponse.legend_query,
|
|
642
|
-
pureExecutionContext,
|
|
643
|
-
dataProductCoordinates,
|
|
644
|
-
config,
|
|
645
|
-
plugin,
|
|
646
|
-
setMessages,
|
|
647
|
-
startTime,
|
|
648
|
-
);
|
|
649
|
-
|
|
650
|
-
if (retryResult.rows.length > 0) {
|
|
651
|
-
await analyzeOrchestratorResults(
|
|
652
|
-
question,
|
|
653
|
-
retryResponse.legend_query,
|
|
654
|
-
retryResult,
|
|
655
|
-
metadata,
|
|
656
|
-
context,
|
|
657
|
-
startTime,
|
|
658
|
-
);
|
|
659
|
-
return;
|
|
660
|
-
}
|
|
661
|
-
} catch {
|
|
662
|
-
/* empty */
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
addThinkingStep(
|
|
668
|
-
setMessages,
|
|
669
|
-
'No results returned \u2014 building contextual guidance...',
|
|
670
|
-
);
|
|
671
|
-
updateLastAssistant(setMessages, () => ({
|
|
672
|
-
isProcessing: true,
|
|
673
|
-
}));
|
|
674
|
-
const fallback = await withTimeout(
|
|
675
|
-
plugin.buildNoResultsFallback(question, legendQuery, metadata, config),
|
|
676
|
-
ANALYSIS_TIMEOUT_MS,
|
|
677
|
-
);
|
|
678
|
-
if (fallback) {
|
|
679
|
-
completeThinkingSteps(setMessages);
|
|
680
|
-
updateLastAssistant(setMessages, () => ({
|
|
681
|
-
textAnswer: fallback.summary,
|
|
682
|
-
suggestedQueries: fallback.suggestedQueries,
|
|
683
|
-
isProcessing: false,
|
|
684
|
-
thinkingDuration: elapsedSeconds(startTime),
|
|
685
|
-
}));
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
export async function processQuestionViaOrchestrator(
|
|
690
|
-
question: string,
|
|
691
|
-
dataProductCoordinates: LegendAIOrchestratorDataProductCoordinates,
|
|
692
|
-
metadata: LegendAIProductMetadata,
|
|
693
|
-
context: LegendAIOperationContext,
|
|
694
|
-
pureExecutionContext?: QueryExplicitExecutionContextInfo,
|
|
695
|
-
preResolvedEntities?: LegendAIResolvedEntities,
|
|
696
|
-
): Promise<void> {
|
|
697
|
-
const { config, plugin, setMessages } = context;
|
|
698
|
-
const startTime = Date.now();
|
|
699
|
-
|
|
700
|
-
try {
|
|
701
|
-
let resolvedEntities: LegendAIResolvedEntities;
|
|
702
|
-
if (preResolvedEntities) {
|
|
703
|
-
resolvedEntities = preResolvedEntities;
|
|
704
|
-
addThinkingStep(
|
|
705
|
-
setMessages,
|
|
706
|
-
`Using pre-resolved root entity: ${resolvedEntities.rootEntity.split('::').pop() ?? resolvedEntities.rootEntity}`,
|
|
707
|
-
);
|
|
708
|
-
} else {
|
|
709
|
-
addThinkingStep(setMessages, 'Resolving entities for your query...');
|
|
710
|
-
resolvedEntities = await plugin.resolveEntitiesForQuery(
|
|
711
|
-
question,
|
|
712
|
-
dataProductCoordinates,
|
|
713
|
-
config,
|
|
714
|
-
pureExecutionContext,
|
|
715
|
-
);
|
|
716
|
-
addThinkingStep(
|
|
717
|
-
setMessages,
|
|
718
|
-
`Found root entity: ${resolvedEntities.rootEntity.split('::').pop() ?? resolvedEntities.rootEntity}`,
|
|
719
|
-
);
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
if (resolvedEntities.relatedEntities.length > 0) {
|
|
723
|
-
addThinkingStep(
|
|
724
|
-
setMessages,
|
|
725
|
-
`Found ${resolvedEntities.relatedEntities.length} related entities`,
|
|
726
|
-
);
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
addThinkingStep(setMessages, 'Generating Legend query via orchestrator...');
|
|
730
|
-
const orchestratorResponse = await plugin.generateQueryViaOrchestrator(
|
|
731
|
-
{
|
|
732
|
-
user_question: question,
|
|
733
|
-
semantic_search_resolution_details: {
|
|
734
|
-
data_product_coordinates: dataProductCoordinates,
|
|
735
|
-
root_entity: resolvedEntities.rootEntity,
|
|
736
|
-
related_entities: resolvedEntities.relatedEntities,
|
|
737
|
-
},
|
|
738
|
-
},
|
|
739
|
-
config,
|
|
740
|
-
);
|
|
741
|
-
|
|
742
|
-
const queryGenTime = elapsedSeconds(startTime, 2);
|
|
743
|
-
completeThinkingSteps(setMessages);
|
|
744
|
-
updateLastAssistant(setMessages, () => ({
|
|
745
|
-
sql: orchestratorResponse.legend_query,
|
|
746
|
-
sqlGenTime: queryGenTime,
|
|
747
|
-
isExecuting: true,
|
|
748
|
-
isProcessing: true,
|
|
749
|
-
}));
|
|
750
|
-
|
|
751
|
-
if (!pureExecutionContext) {
|
|
752
|
-
updateLastAssistant(setMessages, () => ({
|
|
753
|
-
isProcessing: false,
|
|
754
|
-
isExecuting: false,
|
|
755
|
-
error:
|
|
756
|
-
'No execution context available — cannot execute query via engine.',
|
|
757
|
-
errorType: LegendAIErrorType.EXECUTION,
|
|
758
|
-
thinkingDuration: elapsedSeconds(startTime),
|
|
759
|
-
}));
|
|
760
|
-
return;
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
const execResult = await executePureQueryAndReport(
|
|
764
|
-
orchestratorResponse.legend_query,
|
|
765
|
-
pureExecutionContext,
|
|
766
|
-
dataProductCoordinates,
|
|
767
|
-
config,
|
|
768
|
-
plugin,
|
|
769
|
-
setMessages,
|
|
770
|
-
startTime,
|
|
771
|
-
);
|
|
772
|
-
|
|
773
|
-
try {
|
|
774
|
-
if (execResult.rows.length > 0) {
|
|
775
|
-
await analyzeOrchestratorResults(
|
|
776
|
-
question,
|
|
777
|
-
orchestratorResponse.legend_query,
|
|
778
|
-
execResult,
|
|
779
|
-
metadata,
|
|
780
|
-
context,
|
|
781
|
-
startTime,
|
|
782
|
-
);
|
|
783
|
-
} else {
|
|
784
|
-
await handleEmptyOrchestratorResults(
|
|
785
|
-
question,
|
|
786
|
-
orchestratorResponse.legend_query,
|
|
787
|
-
{ dataProductCoordinates, pureExecutionContext },
|
|
788
|
-
metadata,
|
|
789
|
-
resolvedEntities,
|
|
790
|
-
context,
|
|
791
|
-
startTime,
|
|
792
|
-
);
|
|
793
|
-
}
|
|
794
|
-
} catch {
|
|
795
|
-
/* empty */
|
|
796
|
-
} finally {
|
|
797
|
-
completeThinkingSteps(setMessages);
|
|
798
|
-
updateLastAssistant(setMessages, () => ({
|
|
799
|
-
isProcessing: false,
|
|
800
|
-
thinkingDuration: elapsedSeconds(startTime),
|
|
801
|
-
}));
|
|
802
|
-
}
|
|
803
|
-
} catch (error) {
|
|
804
|
-
assertErrorThrown(error);
|
|
805
|
-
const orchErrorType = classifyError(error);
|
|
806
|
-
addThinkingStep(
|
|
807
|
-
setMessages,
|
|
808
|
-
`Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
809
|
-
);
|
|
810
|
-
|
|
811
|
-
try {
|
|
812
|
-
addThinkingStep(
|
|
813
|
-
setMessages,
|
|
814
|
-
'Building guidance from available metadata...',
|
|
815
|
-
);
|
|
816
|
-
const fallbackText = await withTimeout(
|
|
817
|
-
plugin.buildFailureFallback(question, error.message, metadata, config),
|
|
818
|
-
ANALYSIS_TIMEOUT_MS,
|
|
819
|
-
);
|
|
820
|
-
if (fallbackText) {
|
|
821
|
-
completeThinkingSteps(setMessages);
|
|
822
|
-
updateLastAssistant(setMessages, () => ({
|
|
823
|
-
dataContext: fallbackText,
|
|
824
|
-
isProcessing: false,
|
|
825
|
-
thinkingDuration: elapsedSeconds(startTime),
|
|
826
|
-
}));
|
|
827
|
-
return;
|
|
828
|
-
}
|
|
829
|
-
} catch {
|
|
830
|
-
/* empty */
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
finishWithThinkingError(
|
|
834
|
-
setMessages,
|
|
835
|
-
error.message,
|
|
836
|
-
startTime,
|
|
837
|
-
orchErrorType,
|
|
838
|
-
);
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
function cleanLlmSqlResponse(raw: string): string {
|
|
843
|
-
return raw
|
|
844
|
-
.trim()
|
|
845
|
-
.replace(/^```\w*\n?/, '')
|
|
846
|
-
.replace(/\n?```$/, '')
|
|
847
|
-
.replace(/;\s*$/, '')
|
|
848
|
-
.trim();
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
function isValidSqlCorrection(trimmed: string, currentSql: string): boolean {
|
|
852
|
-
return (
|
|
853
|
-
trimmed.length > 0 &&
|
|
854
|
-
trimmed.toLowerCase().startsWith('select') &&
|
|
855
|
-
trimmed !== currentSql
|
|
856
|
-
);
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
const ALIAS_DOT_COL_PATTERN = /\b(?<tbl>[a-z]\w*)\s*\.\s*"(?<col>[^"]+)"/gi;
|
|
860
|
-
|
|
861
|
-
const JOIN_PATTERN = /\bJOIN\b/i;
|
|
862
|
-
|
|
863
|
-
const ORDER_BY_SPLIT = /\bORDER\s+BY\b/i;
|
|
864
|
-
|
|
865
|
-
export function sanitizeJoinOrderBy(sql: string): string {
|
|
866
|
-
if (!JOIN_PATTERN.test(sql)) {
|
|
867
|
-
return sql;
|
|
868
|
-
}
|
|
869
|
-
const parts = sql.split(ORDER_BY_SPLIT);
|
|
870
|
-
if (parts.length < 2) {
|
|
871
|
-
return sql;
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
const beforeOrderBy = parts[0] ?? '';
|
|
875
|
-
const afterOrderBy = parts.slice(1).join('ORDER BY').replace(/^\s+/, '');
|
|
876
|
-
|
|
877
|
-
const selectAliases = new Map<string, string>();
|
|
878
|
-
const aliasRegex =
|
|
879
|
-
/\b(?<tbl>[a-z]\w*)\s*\.\s*"(?<col>[^"]+)"\s+AS\s+(?:"(?<qAlias>[^"]+)"|(?<uAlias>\w+))/gi;
|
|
880
|
-
let m: RegExpExecArray | null;
|
|
881
|
-
while ((m = aliasRegex.exec(beforeOrderBy)) !== null) {
|
|
882
|
-
const tableAlias = (m.groups?.tbl ?? '').toLowerCase();
|
|
883
|
-
const colName = (m.groups?.col ?? '').toLowerCase();
|
|
884
|
-
const asAlias = m.groups?.qAlias ?? m.groups?.uAlias ?? '';
|
|
885
|
-
selectAliases.set(`${tableAlias}.${colName}`, asAlias);
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
if (selectAliases.size === 0) {
|
|
889
|
-
return sql;
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
const rewritten = afterOrderBy.replaceAll(
|
|
893
|
-
ALIAS_DOT_COL_PATTERN,
|
|
894
|
-
(...args) => {
|
|
895
|
-
const groups = args[args.length - 1] as {
|
|
896
|
-
tbl: string;
|
|
897
|
-
col: string;
|
|
898
|
-
};
|
|
899
|
-
const key = `${groups.tbl.toLowerCase()}.${groups.col.toLowerCase()}`;
|
|
900
|
-
const alias = selectAliases.get(key);
|
|
901
|
-
return alias ? `"${alias}"` : String(args[0]);
|
|
902
|
-
},
|
|
903
|
-
);
|
|
904
|
-
|
|
905
|
-
if (rewritten === afterOrderBy) {
|
|
906
|
-
return sql;
|
|
907
|
-
}
|
|
908
|
-
return `${beforeOrderBy}ORDER BY ${rewritten}`;
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
const UNION_ALL_PATTERN = /\bUNION\s+ALL\b/i;
|
|
912
|
-
|
|
913
|
-
const LITERAL_COL_PATTERN = /,\s*'[^']*'\s+AS\s+(?:"[^"]+"|[a-z]\w*)/gi;
|
|
914
|
-
|
|
915
|
-
export function sanitizeLiteralColumns(sql: string): string {
|
|
916
|
-
if (!UNION_ALL_PATTERN.test(sql)) {
|
|
917
|
-
return sql;
|
|
918
|
-
}
|
|
919
|
-
LITERAL_COL_PATTERN.lastIndex = 0;
|
|
920
|
-
if (!LITERAL_COL_PATTERN.test(sql)) {
|
|
921
|
-
return sql;
|
|
922
|
-
}
|
|
923
|
-
LITERAL_COL_PATTERN.lastIndex = 0;
|
|
924
|
-
return sql.replace(LITERAL_COL_PATTERN, '');
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
const SERVICE_PARAM_DATE_LIKE =
|
|
928
|
-
/date|time|day|month|year|period|asOf|businessDate|processingDate|snapshot/i;
|
|
929
|
-
|
|
930
|
-
function hasUnresolvableParams(service: TDSServiceSchema): boolean {
|
|
931
|
-
return service.parameters.some((p) => !SERVICE_PARAM_DATE_LIKE.test(p));
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
function getNonDateParamNames(service: TDSServiceSchema): string[] {
|
|
935
|
-
return service.parameters.filter((p) => !SERVICE_PARAM_DATE_LIKE.test(p));
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
export function stripNonDateServiceParams(sql: string): string {
|
|
939
|
-
return sql.replaceAll(/,\s*\w+\s*=>\s*'[^']*'/g, (match) => {
|
|
940
|
-
const paramName = /,\s*(?<param>\w+)\s*=>/.exec(match)?.groups?.param;
|
|
941
|
-
if (!paramName) {
|
|
942
|
-
return match;
|
|
943
|
-
}
|
|
944
|
-
if (
|
|
945
|
-
paramName === 'coordinates' ||
|
|
946
|
-
SERVICE_PARAM_DATE_LIKE.test(paramName)
|
|
947
|
-
) {
|
|
948
|
-
return match;
|
|
949
|
-
}
|
|
950
|
-
return '';
|
|
951
|
-
});
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
async function executeSqlForServices(
|
|
955
|
-
sql: string,
|
|
956
|
-
services: TDSServiceSchema[],
|
|
957
|
-
dataProductCoordinates:
|
|
958
|
-
| LegendAIOrchestratorDataProductCoordinates
|
|
959
|
-
| undefined,
|
|
960
|
-
plugin: LegendAI_LegendApplicationPlugin_Extension,
|
|
961
|
-
config: LegendAIConfig,
|
|
962
|
-
): Promise<LegendAISqlExecutionResultData> {
|
|
963
|
-
const safeSql = sanitizeLiteralColumns(sanitizeJoinOrderBy(sql));
|
|
964
|
-
const isAccessPoint = services.some(
|
|
965
|
-
(s) => s.sourceType === TDSServiceSourceType.ACCESS_POINT,
|
|
966
|
-
);
|
|
967
|
-
if (isAccessPoint && dataProductCoordinates) {
|
|
968
|
-
return plugin.executeLakehouseSql(safeSql, dataProductCoordinates, config);
|
|
969
|
-
}
|
|
970
|
-
return plugin.executeSql(safeSql, config);
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
interface SqlExecutionOutcome {
|
|
974
|
-
sql: string;
|
|
975
|
-
result?: LegendAISqlExecutionResultData;
|
|
976
|
-
error?: string;
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
async function executeSqlWithRetries(
|
|
980
|
-
initialSql: string,
|
|
981
|
-
question: string,
|
|
982
|
-
services: TDSServiceSchema[],
|
|
983
|
-
coordinates: string,
|
|
984
|
-
dataProductCoordinates:
|
|
985
|
-
| LegendAIOrchestratorDataProductCoordinates
|
|
986
|
-
| undefined,
|
|
987
|
-
context: LegendAIOperationContext,
|
|
988
|
-
): Promise<SqlExecutionOutcome> {
|
|
989
|
-
const { plugin, config, setMessages } = context;
|
|
990
|
-
let currentSql = initialSql;
|
|
991
|
-
|
|
992
|
-
for (let attempt = 0; attempt <= DEFAULT_MAX_EXECUTION_RETRIES; attempt++) {
|
|
993
|
-
try {
|
|
994
|
-
const result = await executeSqlForServices(
|
|
995
|
-
currentSql,
|
|
996
|
-
services,
|
|
997
|
-
dataProductCoordinates,
|
|
998
|
-
plugin,
|
|
999
|
-
config,
|
|
1000
|
-
);
|
|
1001
|
-
return { sql: currentSql, result };
|
|
1002
|
-
} catch (executeError) {
|
|
1003
|
-
assertErrorThrown(executeError);
|
|
1004
|
-
if (attempt >= DEFAULT_MAX_EXECUTION_RETRIES) {
|
|
1005
|
-
return { sql: currentSql, error: executeError.message };
|
|
1006
|
-
}
|
|
1007
|
-
addThinkingStep(
|
|
1008
|
-
setMessages,
|
|
1009
|
-
`Execution failed (attempt ${attempt + 1}/${DEFAULT_MAX_EXECUTION_RETRIES + 1}), correcting query...`,
|
|
1010
|
-
);
|
|
1011
|
-
const corrected = await attemptErrorCorrection(
|
|
1012
|
-
currentSql,
|
|
1013
|
-
executeError.message,
|
|
1014
|
-
question,
|
|
1015
|
-
services,
|
|
1016
|
-
coordinates,
|
|
1017
|
-
plugin,
|
|
1018
|
-
config,
|
|
1019
|
-
);
|
|
1020
|
-
if (corrected) {
|
|
1021
|
-
currentSql = corrected;
|
|
1022
|
-
updateLastAssistant(setMessages, () => ({ sql: currentSql }));
|
|
1023
|
-
continue;
|
|
1024
|
-
}
|
|
1025
|
-
return { sql: currentSql, error: executeError.message };
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
return { sql: currentSql };
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
async function attemptErrorCorrection(
|
|
1032
|
-
currentSql: string,
|
|
1033
|
-
errorMessage: string,
|
|
1034
|
-
question: string,
|
|
1035
|
-
services: TDSServiceSchema[],
|
|
1036
|
-
coordinates: string,
|
|
1037
|
-
plugin: LegendAI_LegendApplicationPlugin_Extension,
|
|
1038
|
-
config: LegendAIConfig,
|
|
1039
|
-
): Promise<string | undefined> {
|
|
1040
|
-
const prompt = plugin.buildErrorCorrectionPrompt(
|
|
1041
|
-
currentSql,
|
|
1042
|
-
errorMessage,
|
|
1043
|
-
question,
|
|
1044
|
-
services,
|
|
1045
|
-
coordinates,
|
|
1046
|
-
);
|
|
1047
|
-
if (!prompt) {
|
|
1048
|
-
return undefined;
|
|
1049
|
-
}
|
|
1050
|
-
try {
|
|
1051
|
-
const correctedSql = await plugin.callLLM(prompt, config);
|
|
1052
|
-
const trimmed = cleanLlmSqlResponse(correctedSql);
|
|
1053
|
-
if (isValidSqlCorrection(trimmed, currentSql)) {
|
|
1054
|
-
return trimmed;
|
|
1055
|
-
}
|
|
1056
|
-
} catch {
|
|
1057
|
-
/* empty */
|
|
1058
|
-
}
|
|
1059
|
-
return undefined;
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
async function attemptZeroRowCorrection(
|
|
1063
|
-
currentSql: string,
|
|
1064
|
-
question: string,
|
|
1065
|
-
services: TDSServiceSchema[],
|
|
1066
|
-
coordinates: string,
|
|
1067
|
-
dataProductCoordinates:
|
|
1068
|
-
| LegendAIOrchestratorDataProductCoordinates
|
|
1069
|
-
| undefined,
|
|
1070
|
-
context: LegendAIOperationContext,
|
|
1071
|
-
): Promise<
|
|
1072
|
-
{ sql: string; result: LegendAISqlExecutionResultData } | undefined
|
|
1073
|
-
> {
|
|
1074
|
-
const { plugin, config, setMessages } = context;
|
|
1075
|
-
addThinkingStep(
|
|
1076
|
-
setMessages,
|
|
1077
|
-
'Query returned 0 rows, attempting filter correction...',
|
|
1078
|
-
);
|
|
1079
|
-
const prompt = plugin.buildZeroRowCorrectionPrompt(
|
|
1080
|
-
currentSql,
|
|
1081
|
-
question,
|
|
1082
|
-
services,
|
|
1083
|
-
coordinates,
|
|
1084
|
-
);
|
|
1085
|
-
if (!prompt) {
|
|
1086
|
-
return undefined;
|
|
1087
|
-
}
|
|
1088
|
-
try {
|
|
1089
|
-
const correctedSql = await plugin.callLLM(prompt, config);
|
|
1090
|
-
const trimmed = cleanLlmSqlResponse(correctedSql);
|
|
1091
|
-
if (!isValidSqlCorrection(trimmed, currentSql)) {
|
|
1092
|
-
return undefined;
|
|
1093
|
-
}
|
|
1094
|
-
addThinkingStep(setMessages, 'Retrying with corrected filters...');
|
|
1095
|
-
updateLastAssistant(setMessages, () => ({ sql: trimmed }));
|
|
1096
|
-
try {
|
|
1097
|
-
const retryResult = await executeSqlForServices(
|
|
1098
|
-
trimmed,
|
|
1099
|
-
services,
|
|
1100
|
-
dataProductCoordinates,
|
|
1101
|
-
plugin,
|
|
1102
|
-
config,
|
|
1103
|
-
);
|
|
1104
|
-
if (retryResult.rows.length > 0) {
|
|
1105
|
-
return { sql: trimmed, result: retryResult };
|
|
1106
|
-
}
|
|
1107
|
-
} catch {
|
|
1108
|
-
/* empty */
|
|
1109
|
-
}
|
|
1110
|
-
} catch {
|
|
1111
|
-
/* empty */
|
|
1112
|
-
}
|
|
1113
|
-
return undefined;
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
function buildZeroRowMessage(services: TDSServiceSchema[]): string {
|
|
1117
|
-
const withUnresolvable = services.filter((s) => hasUnresolvableParams(s));
|
|
1118
|
-
if (withUnresolvable.length > 0) {
|
|
1119
|
-
const parts: string[] = [];
|
|
1120
|
-
for (const svc of withUnresolvable) {
|
|
1121
|
-
for (const paramName of getNonDateParamNames(svc)) {
|
|
1122
|
-
const matchingCol = svc.columns.find((c) => c.name === paramName);
|
|
1123
|
-
const docHint = matchingCol?.documentation ?? matchingCol?.sampleValues;
|
|
1124
|
-
if (docHint) {
|
|
1125
|
-
parts.push(`**${paramName}** (${docHint})`);
|
|
1126
|
-
} else {
|
|
1127
|
-
parts.push(`**${paramName}**`);
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
const uniqueParts = [...new Set(parts)];
|
|
1132
|
-
const firstSvc = withUnresolvable[0];
|
|
1133
|
-
const firstParam = firstSvc
|
|
1134
|
-
? (getNonDateParamNames(firstSvc)[0] ?? 'parameter')
|
|
1135
|
-
: 'parameter';
|
|
1136
|
-
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]".`;
|
|
1137
|
-
}
|
|
1138
|
-
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.';
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
function offerOrchestratorFallbackMessage(
|
|
1142
|
-
setMessages: MessageSetter,
|
|
1143
|
-
startTime: number,
|
|
1144
|
-
fallbackMessage: string,
|
|
1145
|
-
): void {
|
|
1146
|
-
updateLastAssistant(setMessages, () => ({
|
|
1147
|
-
textAnswer: fallbackMessage,
|
|
1148
|
-
fallbackAction: {
|
|
1149
|
-
label: 'Try Legend AI Orchestrator',
|
|
1150
|
-
actionId: LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
|
|
1151
|
-
},
|
|
1152
|
-
isProcessing: false,
|
|
1153
|
-
thinkingDuration: elapsedSeconds(startTime),
|
|
1154
|
-
}));
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
function reportFatalQueryError(
|
|
1158
|
-
setMessages: MessageSetter,
|
|
1159
|
-
startTime: number,
|
|
1160
|
-
errorMessage: string,
|
|
1161
|
-
errorType: LegendAIErrorType,
|
|
1162
|
-
): void {
|
|
1163
|
-
finishWithThinkingError(setMessages, errorMessage, startTime, errorType);
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
function handleSqlGenerationFailure(
|
|
1167
|
-
setMessages: MessageSetter,
|
|
1168
|
-
startTime: number,
|
|
1169
|
-
hasOrchestratorFallback: boolean,
|
|
1170
|
-
orchestratorMessage: string,
|
|
1171
|
-
errorMessage: string,
|
|
1172
|
-
errorType: LegendAIErrorType,
|
|
1173
|
-
): void {
|
|
1174
|
-
completeThinkingSteps(setMessages);
|
|
1175
|
-
if (hasOrchestratorFallback) {
|
|
1176
|
-
offerOrchestratorFallbackMessage(
|
|
1177
|
-
setMessages,
|
|
1178
|
-
startTime,
|
|
1179
|
-
orchestratorMessage,
|
|
1180
|
-
);
|
|
1181
|
-
} else {
|
|
1182
|
-
reportFatalQueryError(setMessages, startTime, errorMessage, errorType);
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
interface QueryResultReport {
|
|
1187
|
-
currentSql: string;
|
|
1188
|
-
sqlResult: LegendAISqlExecutionResultData;
|
|
1189
|
-
question: string;
|
|
1190
|
-
services: TDSServiceSchema[];
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
async function reportQueryResults(
|
|
1194
|
-
report: QueryResultReport,
|
|
1195
|
-
metadata: LegendAIProductMetadata,
|
|
1196
|
-
context: LegendAIOperationContext,
|
|
1197
|
-
startTime: number,
|
|
1198
|
-
hasOrchestratorFallback: boolean,
|
|
1199
|
-
): Promise<void> {
|
|
1200
|
-
const { currentSql, sqlResult, question, services } = report;
|
|
1201
|
-
const { setMessages } = context;
|
|
1202
|
-
if (sqlResult.rows.length > 0) {
|
|
1203
|
-
const columns = deduplicateColumns(sqlResult.columns);
|
|
1204
|
-
const rows = sqlResult.rows;
|
|
1205
|
-
completeThinkingSteps(setMessages);
|
|
1206
|
-
addThinkingStep(
|
|
1207
|
-
setMessages,
|
|
1208
|
-
`Retrieved ${rows.length} row${rows.length === 1 ? '' : 's'}`,
|
|
1209
|
-
);
|
|
1210
|
-
completeThinkingSteps(setMessages);
|
|
1211
|
-
updateLastAssistant(setMessages, () => ({
|
|
1212
|
-
sql: currentSql,
|
|
1213
|
-
gridData: {
|
|
1214
|
-
columnDefs: buildColumnDefsFromNames(columns),
|
|
1215
|
-
rowData: rows,
|
|
1216
|
-
},
|
|
1217
|
-
execTime: elapsedSeconds(startTime, 2),
|
|
1218
|
-
isProcessing: true,
|
|
1219
|
-
isExecuting: false,
|
|
1220
|
-
thinkingDuration: elapsedSeconds(startTime),
|
|
1221
|
-
}));
|
|
1222
|
-
|
|
1223
|
-
try {
|
|
1224
|
-
await analyzeOrchestratorResults(
|
|
1225
|
-
question,
|
|
1226
|
-
currentSql,
|
|
1227
|
-
sqlResult,
|
|
1228
|
-
metadata,
|
|
1229
|
-
context,
|
|
1230
|
-
startTime,
|
|
1231
|
-
);
|
|
1232
|
-
} catch {
|
|
1233
|
-
/* empty */
|
|
1234
|
-
} finally {
|
|
1235
|
-
completeThinkingSteps(setMessages);
|
|
1236
|
-
updateLastAssistant(setMessages, () => ({
|
|
1237
|
-
isProcessing: false,
|
|
1238
|
-
thinkingDuration: elapsedSeconds(startTime),
|
|
1239
|
-
}));
|
|
1240
|
-
}
|
|
1241
|
-
} else {
|
|
1242
|
-
addThinkingStep(
|
|
1243
|
-
setMessages,
|
|
1244
|
-
'Query returned 0 rows after correction attempts.',
|
|
1245
|
-
);
|
|
1246
|
-
completeThinkingSteps(setMessages);
|
|
1247
|
-
const fallback = hasOrchestratorFallback
|
|
1248
|
-
? {
|
|
1249
|
-
fallbackAction: {
|
|
1250
|
-
label: 'Try Legend AI Orchestrator',
|
|
1251
|
-
actionId: LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
|
|
1252
|
-
} as LegendAIFallbackAction,
|
|
1253
|
-
}
|
|
1254
|
-
: {};
|
|
1255
|
-
updateLastAssistant(setMessages, () => ({
|
|
1256
|
-
textAnswer: buildZeroRowMessage(services),
|
|
1257
|
-
...fallback,
|
|
1258
|
-
isProcessing: false,
|
|
1259
|
-
isExecuting: false,
|
|
1260
|
-
thinkingDuration: elapsedSeconds(startTime),
|
|
1261
|
-
}));
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
async function selectBestServices(
|
|
1266
|
-
question: string,
|
|
1267
|
-
services: TDSServiceSchema[],
|
|
1268
|
-
context: LegendAIOperationContext,
|
|
1269
|
-
): Promise<TDSServiceSchema[]> {
|
|
1270
|
-
const { plugin, config, setMessages } = context;
|
|
1271
|
-
if (services.length <= 1) {
|
|
1272
|
-
return services;
|
|
1273
|
-
}
|
|
1274
|
-
try {
|
|
1275
|
-
addThinkingStep(setMessages, 'Selecting best service for your query...');
|
|
1276
|
-
return await plugin.selectRelevantServices(question, services, config);
|
|
1277
|
-
} catch {
|
|
1278
|
-
return services;
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
async function tryRecoverZeroRows(
|
|
1283
|
-
currentSql: string,
|
|
1284
|
-
sqlResult: LegendAISqlExecutionResultData,
|
|
1285
|
-
question: string,
|
|
1286
|
-
selectedServices: TDSServiceSchema[],
|
|
1287
|
-
coordinates: string,
|
|
1288
|
-
dataProductCoordinates:
|
|
1289
|
-
| LegendAIOrchestratorDataProductCoordinates
|
|
1290
|
-
| undefined,
|
|
1291
|
-
context: LegendAIOperationContext,
|
|
1292
|
-
): Promise<{ sql: string; result: LegendAISqlExecutionResultData }> {
|
|
1293
|
-
const { plugin, config, setMessages } = context;
|
|
1294
|
-
let recoveredSql = currentSql;
|
|
1295
|
-
let recoveredResult = sqlResult;
|
|
1296
|
-
|
|
1297
|
-
const strippedSql = stripNonDateServiceParams(recoveredSql);
|
|
1298
|
-
if (strippedSql !== recoveredSql) {
|
|
1299
|
-
addThinkingStep(
|
|
1300
|
-
setMessages,
|
|
1301
|
-
'Trying query without guessed parameter values...',
|
|
1302
|
-
);
|
|
1303
|
-
try {
|
|
1304
|
-
const strippedResult = await executeSqlForServices(
|
|
1305
|
-
strippedSql,
|
|
1306
|
-
selectedServices,
|
|
1307
|
-
dataProductCoordinates,
|
|
1308
|
-
plugin,
|
|
1309
|
-
config,
|
|
1310
|
-
);
|
|
1311
|
-
if (strippedResult.rows.length > 0) {
|
|
1312
|
-
recoveredSql = strippedSql;
|
|
1313
|
-
recoveredResult = strippedResult;
|
|
1314
|
-
updateLastAssistant(setMessages, () => ({ sql: strippedSql }));
|
|
1315
|
-
}
|
|
1316
|
-
} catch {
|
|
1317
|
-
/* empty */
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
if (recoveredResult.rows.length === 0) {
|
|
1322
|
-
const correction = await attemptZeroRowCorrection(
|
|
1323
|
-
recoveredSql,
|
|
1324
|
-
question,
|
|
1325
|
-
selectedServices,
|
|
1326
|
-
coordinates,
|
|
1327
|
-
dataProductCoordinates,
|
|
1328
|
-
context,
|
|
1329
|
-
);
|
|
1330
|
-
if (correction) {
|
|
1331
|
-
recoveredSql = correction.sql;
|
|
1332
|
-
recoveredResult = correction.result;
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
return { sql: recoveredSql, result: recoveredResult };
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
async function processDataQuery(
|
|
1340
|
-
question: string,
|
|
1341
|
-
services: TDSServiceSchema[],
|
|
1342
|
-
coordinates: string,
|
|
1343
|
-
metadata: LegendAIProductMetadata,
|
|
1344
|
-
context: LegendAIOperationContext,
|
|
1345
|
-
startTime: number,
|
|
1346
|
-
orchestratorOptions?: LegendAIOrchestratorOptionsParam,
|
|
1347
|
-
): Promise<void> {
|
|
1348
|
-
const { config, setMessages } = context;
|
|
1349
|
-
const dataProductCoordinates = orchestratorOptions?.dataProductCoordinates;
|
|
1350
|
-
const hasOrchestratorFallback = Boolean(
|
|
1351
|
-
config.orchestratorUrl && dataProductCoordinates,
|
|
1352
|
-
);
|
|
1353
|
-
|
|
1354
|
-
if (services.length === 0) {
|
|
1355
|
-
handleSqlGenerationFailure(
|
|
1356
|
-
setMessages,
|
|
1357
|
-
startTime,
|
|
1358
|
-
hasOrchestratorFallback,
|
|
1359
|
-
'No TDS services available for SQL querying. You can try the Legend AI Orchestrator to generate a Pure query instead.',
|
|
1360
|
-
'No TDS services available for querying',
|
|
1361
|
-
LegendAIErrorType.GENERAL,
|
|
1362
|
-
);
|
|
1363
|
-
return;
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
addThinkingStep(setMessages, 'Found relevant services to query');
|
|
1367
|
-
|
|
1368
|
-
const selectedServices = await selectBestServices(
|
|
1369
|
-
question,
|
|
1370
|
-
services,
|
|
1371
|
-
context,
|
|
1372
|
-
);
|
|
1373
|
-
|
|
1374
|
-
const judgedSql = await generateAndJudgeSql(
|
|
1375
|
-
question,
|
|
1376
|
-
selectedServices,
|
|
1377
|
-
coordinates,
|
|
1378
|
-
context,
|
|
1379
|
-
startTime,
|
|
1380
|
-
);
|
|
1381
|
-
|
|
1382
|
-
if (!judgedSql) {
|
|
1383
|
-
addThinkingStep(
|
|
1384
|
-
setMessages,
|
|
1385
|
-
'SQL generation could not produce a valid query.',
|
|
1386
|
-
);
|
|
1387
|
-
handleSqlGenerationFailure(
|
|
1388
|
-
setMessages,
|
|
1389
|
-
startTime,
|
|
1390
|
-
hasOrchestratorFallback,
|
|
1391
|
-
'SQL generation could not handle this query. You can try the Legend AI Orchestrator to generate a Pure query instead.',
|
|
1392
|
-
'SQL generation could not handle this query. Try rephrasing your question.',
|
|
1393
|
-
LegendAIErrorType.GENERATION,
|
|
1394
|
-
);
|
|
1395
|
-
return;
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
const sqlGenTimeValue = elapsedSeconds(startTime, 2);
|
|
1399
|
-
completeThinkingSteps(setMessages);
|
|
1400
|
-
updateLastAssistant(setMessages, () => ({
|
|
1401
|
-
sql: judgedSql,
|
|
1402
|
-
sqlGenTime: sqlGenTimeValue,
|
|
1403
|
-
isExecuting: true,
|
|
1404
|
-
}));
|
|
1405
|
-
|
|
1406
|
-
const execOutcome = await executeSqlWithRetries(
|
|
1407
|
-
judgedSql,
|
|
1408
|
-
question,
|
|
1409
|
-
selectedServices,
|
|
1410
|
-
coordinates,
|
|
1411
|
-
dataProductCoordinates,
|
|
1412
|
-
context,
|
|
1413
|
-
);
|
|
1414
|
-
|
|
1415
|
-
if (execOutcome.error) {
|
|
1416
|
-
const execErrorType = classifyError(new Error(execOutcome.error));
|
|
1417
|
-
addThinkingStep(
|
|
1418
|
-
setMessages,
|
|
1419
|
-
`Execution failed: ${execOutcome.error.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
1420
|
-
);
|
|
1421
|
-
finishWithThinkingError(
|
|
1422
|
-
setMessages,
|
|
1423
|
-
buildExecutionErrorMessage(execOutcome.error, selectedServices),
|
|
1424
|
-
startTime,
|
|
1425
|
-
execErrorType === LegendAIErrorType.GENERAL
|
|
1426
|
-
? LegendAIErrorType.EXECUTION
|
|
1427
|
-
: execErrorType,
|
|
1428
|
-
);
|
|
1429
|
-
updateLastAssistant(setMessages, () => ({
|
|
1430
|
-
isExecuting: false,
|
|
1431
|
-
...(hasOrchestratorFallback
|
|
1432
|
-
? {
|
|
1433
|
-
fallbackAction: {
|
|
1434
|
-
label: 'Try Legend AI Orchestrator',
|
|
1435
|
-
actionId: LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
|
|
1436
|
-
} as LegendAIFallbackAction,
|
|
1437
|
-
}
|
|
1438
|
-
: {}),
|
|
1439
|
-
}));
|
|
1440
|
-
return;
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
if (!execOutcome.result) {
|
|
1444
|
-
return;
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
let currentSql = execOutcome.sql;
|
|
1448
|
-
let sqlResult = execOutcome.result;
|
|
1449
|
-
|
|
1450
|
-
if (sqlResult.rows.length === 0) {
|
|
1451
|
-
const recovered = await tryRecoverZeroRows(
|
|
1452
|
-
currentSql,
|
|
1453
|
-
sqlResult,
|
|
1454
|
-
question,
|
|
1455
|
-
selectedServices,
|
|
1456
|
-
coordinates,
|
|
1457
|
-
dataProductCoordinates,
|
|
1458
|
-
context,
|
|
1459
|
-
);
|
|
1460
|
-
currentSql = recovered.sql;
|
|
1461
|
-
sqlResult = recovered.result;
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
await reportQueryResults(
|
|
1465
|
-
{
|
|
1466
|
-
currentSql,
|
|
1467
|
-
sqlResult,
|
|
1468
|
-
question,
|
|
1469
|
-
services: selectedServices,
|
|
1470
|
-
},
|
|
1471
|
-
metadata,
|
|
1472
|
-
context,
|
|
1473
|
-
startTime,
|
|
1474
|
-
hasOrchestratorFallback,
|
|
1475
|
-
);
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
export async function processQuestion(
|
|
1479
|
-
question: string,
|
|
1480
|
-
services: TDSServiceSchema[],
|
|
1481
|
-
coordinates: string,
|
|
1482
|
-
metadata: LegendAIProductMetadata,
|
|
1483
|
-
context: LegendAIOperationContext,
|
|
1484
|
-
dataProductCoordinates?: LegendAIOrchestratorDataProductCoordinates,
|
|
1485
|
-
pureExecutionContext?: QueryExplicitExecutionContextInfo,
|
|
1486
|
-
): Promise<void> {
|
|
1487
|
-
const { config, plugin, setMessages } = context;
|
|
1488
|
-
const startTime = Date.now();
|
|
1489
|
-
|
|
1490
|
-
try {
|
|
1491
|
-
addThinkingStep(setMessages, 'Analyzing your question...');
|
|
1492
|
-
|
|
1493
|
-
const orchestratorOpts = dataProductCoordinates
|
|
1494
|
-
? {
|
|
1495
|
-
dataProductCoordinates,
|
|
1496
|
-
...(pureExecutionContext === undefined
|
|
1497
|
-
? {}
|
|
1498
|
-
: { pureExecutionContext }),
|
|
1499
|
-
}
|
|
1500
|
-
: undefined;
|
|
1501
|
-
|
|
1502
|
-
if (services.length > 0) {
|
|
1503
|
-
const serviceNames = services.map((s) => s.title);
|
|
1504
|
-
const intent = await plugin.classifyQuestionIntent(
|
|
1505
|
-
question,
|
|
1506
|
-
true,
|
|
1507
|
-
config,
|
|
1508
|
-
serviceNames,
|
|
1509
|
-
);
|
|
1510
|
-
|
|
1511
|
-
if (intent === LegendAIQuestionIntent.METADATA) {
|
|
1512
|
-
await handleMetadataQuestion(
|
|
1513
|
-
question,
|
|
1514
|
-
metadata,
|
|
1515
|
-
context,
|
|
1516
|
-
startTime,
|
|
1517
|
-
true,
|
|
1518
|
-
);
|
|
1519
|
-
return;
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
// DATA_QUERY or ORCHESTRATOR — try SQL generation.
|
|
1523
|
-
// If SQL throws, fall back to metadata as a safety net
|
|
1524
|
-
// (e.g. misclassified metadata question).
|
|
1525
|
-
try {
|
|
1526
|
-
await processDataQuery(
|
|
1527
|
-
question,
|
|
1528
|
-
services,
|
|
1529
|
-
coordinates,
|
|
1530
|
-
metadata,
|
|
1531
|
-
context,
|
|
1532
|
-
startTime,
|
|
1533
|
-
orchestratorOpts,
|
|
1534
|
-
);
|
|
1535
|
-
} catch (sqlError) {
|
|
1536
|
-
assertErrorThrown(sqlError);
|
|
1537
|
-
addThinkingStep(
|
|
1538
|
-
setMessages,
|
|
1539
|
-
'SQL generation failed, answering from product metadata...',
|
|
1540
|
-
);
|
|
1541
|
-
await handleMetadataQuestion(
|
|
1542
|
-
question,
|
|
1543
|
-
metadata,
|
|
1544
|
-
context,
|
|
1545
|
-
startTime,
|
|
1546
|
-
true,
|
|
1547
|
-
);
|
|
1548
|
-
}
|
|
1549
|
-
return;
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
// No services available — use orchestrator if configured, else metadata only.
|
|
1553
|
-
if (config.orchestratorUrl && dataProductCoordinates) {
|
|
1554
|
-
completeThinkingSteps(setMessages);
|
|
1555
|
-
await processQuestionViaOrchestrator(
|
|
1556
|
-
question,
|
|
1557
|
-
dataProductCoordinates,
|
|
1558
|
-
metadata,
|
|
1559
|
-
context,
|
|
1560
|
-
pureExecutionContext,
|
|
1561
|
-
);
|
|
1562
|
-
} else {
|
|
1563
|
-
await handleMetadataQuestion(
|
|
1564
|
-
question,
|
|
1565
|
-
metadata,
|
|
1566
|
-
context,
|
|
1567
|
-
startTime,
|
|
1568
|
-
false,
|
|
1569
|
-
);
|
|
1570
|
-
}
|
|
1571
|
-
} catch (error) {
|
|
1572
|
-
assertErrorThrown(error);
|
|
1573
|
-
addThinkingStep(
|
|
1574
|
-
setMessages,
|
|
1575
|
-
`Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
1576
|
-
);
|
|
1577
|
-
finishWithThinkingError(
|
|
1578
|
-
setMessages,
|
|
1579
|
-
error.message,
|
|
1580
|
-
startTime,
|
|
1581
|
-
classifyError(error),
|
|
1582
|
-
);
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
export async function processQuestionWithIntent(
|
|
1587
|
-
question: string,
|
|
1588
|
-
intent: LegendAIQuestionIntent,
|
|
1589
|
-
services: TDSServiceSchema[],
|
|
1590
|
-
coordinates: string,
|
|
1591
|
-
metadata: LegendAIProductMetadata,
|
|
1592
|
-
context: LegendAIOperationContext,
|
|
1593
|
-
orchestratorOptions?: LegendAIOrchestratorOptionsParam,
|
|
1594
|
-
): Promise<void> {
|
|
1595
|
-
const { config, setMessages } = context;
|
|
1596
|
-
const dataProductCoordinates = orchestratorOptions?.dataProductCoordinates;
|
|
1597
|
-
const pureExecutionContext = orchestratorOptions?.pureExecutionContext;
|
|
1598
|
-
|
|
1599
|
-
if (intent === LegendAIQuestionIntent.METADATA) {
|
|
1600
|
-
const startTime = Date.now();
|
|
1601
|
-
try {
|
|
1602
|
-
await handleMetadataQuestion(
|
|
1603
|
-
question,
|
|
1604
|
-
metadata,
|
|
1605
|
-
context,
|
|
1606
|
-
startTime,
|
|
1607
|
-
services.length > 0,
|
|
1608
|
-
);
|
|
1609
|
-
} catch (error) {
|
|
1610
|
-
assertErrorThrown(error);
|
|
1611
|
-
addThinkingStep(
|
|
1612
|
-
setMessages,
|
|
1613
|
-
`Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
1614
|
-
);
|
|
1615
|
-
finishWithThinkingError(
|
|
1616
|
-
setMessages,
|
|
1617
|
-
error.message,
|
|
1618
|
-
startTime,
|
|
1619
|
-
classifyError(error),
|
|
1620
|
-
);
|
|
1621
|
-
}
|
|
1622
|
-
return;
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
if (intent === LegendAIQuestionIntent.ORCHESTRATOR) {
|
|
1626
|
-
if (config.orchestratorUrl && dataProductCoordinates) {
|
|
1627
|
-
// When services are available, try SQL first even for ORCHESTRATOR intent
|
|
1628
|
-
if (services.length > 0) {
|
|
1629
|
-
const startTime = Date.now();
|
|
1630
|
-
try {
|
|
1631
|
-
addThinkingStep(setMessages, 'Preparing data query...');
|
|
1632
|
-
await processDataQuery(
|
|
1633
|
-
question,
|
|
1634
|
-
services,
|
|
1635
|
-
coordinates,
|
|
1636
|
-
metadata,
|
|
1637
|
-
context,
|
|
1638
|
-
startTime,
|
|
1639
|
-
orchestratorOptions,
|
|
1640
|
-
);
|
|
1641
|
-
} catch (error) {
|
|
1642
|
-
assertErrorThrown(error);
|
|
1643
|
-
addThinkingStep(
|
|
1644
|
-
setMessages,
|
|
1645
|
-
`Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
1646
|
-
);
|
|
1647
|
-
finishWithThinkingError(
|
|
1648
|
-
setMessages,
|
|
1649
|
-
error.message,
|
|
1650
|
-
startTime,
|
|
1651
|
-
classifyError(error),
|
|
1652
|
-
);
|
|
1653
|
-
}
|
|
1654
|
-
return;
|
|
1655
|
-
}
|
|
1656
|
-
await processQuestionViaOrchestrator(
|
|
1657
|
-
question,
|
|
1658
|
-
dataProductCoordinates,
|
|
1659
|
-
metadata,
|
|
1660
|
-
context,
|
|
1661
|
-
pureExecutionContext,
|
|
1662
|
-
);
|
|
1663
|
-
return;
|
|
1664
|
-
}
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
const startTime = Date.now();
|
|
1668
|
-
|
|
1669
|
-
try {
|
|
1670
|
-
addThinkingStep(setMessages, 'Preparing data query...');
|
|
1671
|
-
await processDataQuery(
|
|
1672
|
-
question,
|
|
1673
|
-
services,
|
|
1674
|
-
coordinates,
|
|
1675
|
-
metadata,
|
|
1676
|
-
context,
|
|
1677
|
-
startTime,
|
|
1678
|
-
orchestratorOptions,
|
|
1679
|
-
);
|
|
1680
|
-
} catch (error) {
|
|
1681
|
-
assertErrorThrown(error);
|
|
1682
|
-
addThinkingStep(
|
|
1683
|
-
setMessages,
|
|
1684
|
-
`Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
1685
|
-
);
|
|
1686
|
-
finishWithThinkingError(
|
|
1687
|
-
setMessages,
|
|
1688
|
-
error.message,
|
|
1689
|
-
startTime,
|
|
1690
|
-
classifyError(error),
|
|
1691
|
-
);
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
35
|
+
import {
|
|
36
|
+
buildConversationHistory,
|
|
37
|
+
createMessagePair,
|
|
38
|
+
processQuestion,
|
|
39
|
+
processQuestionWithIntent,
|
|
40
|
+
processQuestionViaOrchestrator,
|
|
41
|
+
} from './LegendAIChatProcessors.js';
|
|
1694
42
|
|
|
1695
43
|
export const useLegendAIChatState = (
|
|
1696
44
|
services: TDSServiceSchema[],
|
|
@@ -1701,17 +49,36 @@ export const useLegendAIChatState = (
|
|
|
1701
49
|
dataProductCoordinates?: LegendAIOrchestratorDataProductCoordinates,
|
|
1702
50
|
pureExecutionContext?: QueryExplicitExecutionContextInfo,
|
|
1703
51
|
): LegendAIChatState => {
|
|
52
|
+
const LEGEND_AI_MCP_SCOPE_ID = 'legend-ai-mcp';
|
|
1704
53
|
const [questionText, setQuestionText] = useState('');
|
|
1705
54
|
const [isSending, setIsSending] = useState(false);
|
|
1706
55
|
const [messages, setMessages] = useState<LegendAIMessage[]>([]);
|
|
1707
56
|
const [expandedThinking, setExpandedThinking] = useState<Set<number>>(
|
|
1708
57
|
new Set(),
|
|
1709
58
|
);
|
|
59
|
+
const [selectedScopes, setSelectedScopes] = useState<LegendAIScopeItem[]>([]);
|
|
60
|
+
const [selectedModelName, setSelectedModelName] = useState<
|
|
61
|
+
string | undefined
|
|
62
|
+
>(undefined);
|
|
63
|
+
|
|
64
|
+
const availableModelNames = useMemo(() => {
|
|
65
|
+
const names = new Set<string>();
|
|
66
|
+
if (config.llmModelName) {
|
|
67
|
+
names.add(config.llmModelName);
|
|
68
|
+
}
|
|
69
|
+
for (const modelName of config.llmModelOptions ?? []) {
|
|
70
|
+
if (modelName.trim()) {
|
|
71
|
+
names.add(modelName.trim());
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return Array.from(names);
|
|
75
|
+
}, [config.llmModelName, config.llmModelOptions]);
|
|
1710
76
|
|
|
1711
77
|
const conversationRef = useRef<HTMLDivElement>(null);
|
|
1712
78
|
const sendTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(
|
|
1713
79
|
undefined,
|
|
1714
80
|
);
|
|
81
|
+
const cancelledRef = useRef(false);
|
|
1715
82
|
|
|
1716
83
|
useEffect(() => {
|
|
1717
84
|
const el = conversationRef.current;
|
|
@@ -1740,6 +107,47 @@ export const useLegendAIChatState = (
|
|
|
1740
107
|
});
|
|
1741
108
|
}, []);
|
|
1742
109
|
|
|
110
|
+
const toggleScope = useCallback((scope: LegendAIScopeItem) => {
|
|
111
|
+
setSelectedScopes((prev) =>
|
|
112
|
+
prev.some((s) => s.id === scope.id)
|
|
113
|
+
? prev.filter((s) => s.id !== scope.id)
|
|
114
|
+
: [...prev, scope],
|
|
115
|
+
);
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
118
|
+
const removeScope = useCallback((scopeId: string) => {
|
|
119
|
+
setSelectedScopes((prev) => prev.filter((s) => s.id !== scopeId));
|
|
120
|
+
}, []);
|
|
121
|
+
|
|
122
|
+
const configForRequest = useMemo(
|
|
123
|
+
() => ({
|
|
124
|
+
...config,
|
|
125
|
+
llmModelName: selectedModelName ?? config.llmModelName,
|
|
126
|
+
}),
|
|
127
|
+
[config, selectedModelName],
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const stopGeneration = useCallback(() => {
|
|
131
|
+
cancelledRef.current = true;
|
|
132
|
+
if (sendTimeoutRef.current !== undefined) {
|
|
133
|
+
clearTimeout(sendTimeoutRef.current);
|
|
134
|
+
sendTimeoutRef.current = undefined;
|
|
135
|
+
}
|
|
136
|
+
setIsSending(false);
|
|
137
|
+
setMessages((prev) => {
|
|
138
|
+
const updated = [...prev];
|
|
139
|
+
const last = updated[updated.length - 1];
|
|
140
|
+
if (last?.role === LegendAIMessageRole.ASSISTANT) {
|
|
141
|
+
const stopped = { ...last, isProcessing: false, isExecuting: false };
|
|
142
|
+
if (!stopped.textAnswer && !stopped.sql && !stopped.error) {
|
|
143
|
+
stopped.textAnswer = 'Generation stopped.';
|
|
144
|
+
}
|
|
145
|
+
updated[updated.length - 1] = stopped;
|
|
146
|
+
}
|
|
147
|
+
return updated;
|
|
148
|
+
});
|
|
149
|
+
}, []);
|
|
150
|
+
|
|
1743
151
|
const clearChat = useCallback(() => {
|
|
1744
152
|
setMessages([]);
|
|
1745
153
|
setExpandedThinking(new Set());
|
|
@@ -1765,6 +173,7 @@ export const useLegendAIChatState = (
|
|
|
1765
173
|
}
|
|
1766
174
|
const history = buildConversationHistory(messages);
|
|
1767
175
|
setIsSending(true);
|
|
176
|
+
cancelledRef.current = false;
|
|
1768
177
|
setQuestionText('');
|
|
1769
178
|
setMessages((prev) => [...prev, ...createMessagePair(trimmed)]);
|
|
1770
179
|
if (sendTimeoutRef.current !== undefined) {
|
|
@@ -1773,9 +182,11 @@ export const useLegendAIChatState = (
|
|
|
1773
182
|
}
|
|
1774
183
|
sendTimeoutRef.current = setTimeout(() => {
|
|
1775
184
|
process(trimmed, history)
|
|
1776
|
-
.catch(noop
|
|
185
|
+
.catch(noop)
|
|
1777
186
|
.finally(() => {
|
|
1778
|
-
|
|
187
|
+
if (!cancelledRef.current) {
|
|
188
|
+
setIsSending(false);
|
|
189
|
+
}
|
|
1779
190
|
sendTimeoutRef.current = undefined;
|
|
1780
191
|
});
|
|
1781
192
|
}, 0);
|
|
@@ -1786,58 +197,100 @@ export const useLegendAIChatState = (
|
|
|
1786
197
|
const askQuestion = useCallback(
|
|
1787
198
|
(): void =>
|
|
1788
199
|
dispatchQuestion(questionText, (trimmed, history) =>
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
200
|
+
selectedScopes.some((scope) => scope.id === LEGEND_AI_MCP_SCOPE_ID) &&
|
|
201
|
+
configForRequest.orchestratorUrl &&
|
|
202
|
+
dataProductCoordinates
|
|
203
|
+
? processQuestionViaOrchestrator(
|
|
204
|
+
trimmed,
|
|
205
|
+
dataProductCoordinates,
|
|
206
|
+
metadata,
|
|
207
|
+
{
|
|
208
|
+
config: configForRequest,
|
|
209
|
+
plugin,
|
|
210
|
+
history,
|
|
211
|
+
setMessages,
|
|
212
|
+
},
|
|
213
|
+
pureExecutionContext,
|
|
214
|
+
)
|
|
215
|
+
: processQuestion(
|
|
216
|
+
trimmed,
|
|
217
|
+
services,
|
|
218
|
+
coordinates,
|
|
219
|
+
metadata,
|
|
220
|
+
{
|
|
221
|
+
config: configForRequest,
|
|
222
|
+
plugin,
|
|
223
|
+
history,
|
|
224
|
+
setMessages,
|
|
225
|
+
},
|
|
226
|
+
dataProductCoordinates,
|
|
227
|
+
pureExecutionContext,
|
|
228
|
+
),
|
|
1798
229
|
),
|
|
1799
230
|
[
|
|
1800
231
|
questionText,
|
|
1801
232
|
dispatchQuestion,
|
|
1802
233
|
services,
|
|
1803
234
|
coordinates,
|
|
1804
|
-
|
|
235
|
+
configForRequest,
|
|
1805
236
|
metadata,
|
|
1806
237
|
plugin,
|
|
1807
238
|
dataProductCoordinates,
|
|
1808
239
|
pureExecutionContext,
|
|
240
|
+
selectedScopes,
|
|
1809
241
|
],
|
|
1810
242
|
);
|
|
1811
243
|
|
|
1812
244
|
const askQuestionWithIntent = useCallback(
|
|
1813
245
|
(text: string, intent: LegendAIQuestionIntent): void =>
|
|
1814
246
|
dispatchQuestion(text, (trimmed, history) =>
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
247
|
+
selectedScopes.some((scope) => scope.id === LEGEND_AI_MCP_SCOPE_ID) &&
|
|
248
|
+
configForRequest.orchestratorUrl &&
|
|
249
|
+
dataProductCoordinates
|
|
250
|
+
? processQuestionViaOrchestrator(
|
|
251
|
+
trimmed,
|
|
252
|
+
dataProductCoordinates,
|
|
253
|
+
metadata,
|
|
254
|
+
{
|
|
255
|
+
config: configForRequest,
|
|
256
|
+
plugin,
|
|
257
|
+
history,
|
|
258
|
+
setMessages,
|
|
259
|
+
},
|
|
260
|
+
pureExecutionContext,
|
|
261
|
+
)
|
|
262
|
+
: processQuestionWithIntent(
|
|
263
|
+
trimmed,
|
|
264
|
+
intent,
|
|
265
|
+
services,
|
|
266
|
+
coordinates,
|
|
267
|
+
metadata,
|
|
268
|
+
{
|
|
269
|
+
config: configForRequest,
|
|
270
|
+
plugin,
|
|
271
|
+
history,
|
|
272
|
+
setMessages,
|
|
273
|
+
},
|
|
274
|
+
dataProductCoordinates
|
|
275
|
+
? {
|
|
276
|
+
dataProductCoordinates,
|
|
277
|
+
...(pureExecutionContext === undefined
|
|
278
|
+
? {}
|
|
279
|
+
: { pureExecutionContext }),
|
|
280
|
+
}
|
|
281
|
+
: undefined,
|
|
282
|
+
),
|
|
1831
283
|
),
|
|
1832
284
|
[
|
|
1833
285
|
dispatchQuestion,
|
|
1834
286
|
services,
|
|
1835
287
|
coordinates,
|
|
1836
|
-
|
|
288
|
+
configForRequest,
|
|
1837
289
|
metadata,
|
|
1838
290
|
plugin,
|
|
1839
291
|
dataProductCoordinates,
|
|
1840
292
|
pureExecutionContext,
|
|
293
|
+
selectedScopes,
|
|
1841
294
|
],
|
|
1842
295
|
);
|
|
1843
296
|
|
|
@@ -1846,7 +299,6 @@ export const useLegendAIChatState = (
|
|
|
1846
299
|
if (isSending || !config.orchestratorUrl || !dataProductCoordinates) {
|
|
1847
300
|
return;
|
|
1848
301
|
}
|
|
1849
|
-
// Find the user question associated with this assistant message
|
|
1850
302
|
let question: string | undefined;
|
|
1851
303
|
for (let i = 0; i < messages.length; i++) {
|
|
1852
304
|
const msg = messages[i];
|
|
@@ -1875,15 +327,19 @@ export const useLegendAIChatState = (
|
|
|
1875
327
|
);
|
|
1876
328
|
|
|
1877
329
|
const history = buildConversationHistory(messages);
|
|
1878
|
-
const q = question;
|
|
1879
330
|
processQuestionViaOrchestrator(
|
|
1880
|
-
|
|
331
|
+
question,
|
|
1881
332
|
dataProductCoordinates,
|
|
1882
333
|
metadata,
|
|
1883
|
-
{
|
|
334
|
+
{
|
|
335
|
+
config: configForRequest,
|
|
336
|
+
plugin,
|
|
337
|
+
history,
|
|
338
|
+
setMessages,
|
|
339
|
+
},
|
|
1884
340
|
pureExecutionContext,
|
|
1885
341
|
)
|
|
1886
|
-
.catch(noop
|
|
342
|
+
.catch(noop)
|
|
1887
343
|
.finally(() => {
|
|
1888
344
|
setIsSending(false);
|
|
1889
345
|
});
|
|
@@ -1892,6 +348,7 @@ export const useLegendAIChatState = (
|
|
|
1892
348
|
isSending,
|
|
1893
349
|
messages,
|
|
1894
350
|
config,
|
|
351
|
+
configForRequest,
|
|
1895
352
|
metadata,
|
|
1896
353
|
plugin,
|
|
1897
354
|
dataProductCoordinates,
|
|
@@ -1904,6 +361,9 @@ export const useLegendAIChatState = (
|
|
|
1904
361
|
setQuestionText,
|
|
1905
362
|
isSending,
|
|
1906
363
|
messages,
|
|
364
|
+
selectedModelName,
|
|
365
|
+
availableModelNames,
|
|
366
|
+
setSelectedModelName,
|
|
1907
367
|
askQuestion,
|
|
1908
368
|
askQuestionWithIntent,
|
|
1909
369
|
runFallbackAction,
|
|
@@ -1911,5 +371,9 @@ export const useLegendAIChatState = (
|
|
|
1911
371
|
expandedThinking,
|
|
1912
372
|
toggleThinking,
|
|
1913
373
|
conversationRef,
|
|
374
|
+
selectedScopes,
|
|
375
|
+
toggleScope,
|
|
376
|
+
removeScope,
|
|
377
|
+
stopGeneration,
|
|
1914
378
|
};
|
|
1915
379
|
};
|