@finos/legend-lego 2.0.195 → 2.0.196
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.css +2 -2
- package/lib/index.css.map +1 -1
- package/lib/legend-ai/LegendAITypes.d.ts +33 -0
- package/lib/legend-ai/LegendAITypes.d.ts.map +1 -1
- package/lib/legend-ai/LegendAITypes.js +39 -1
- package/lib/legend-ai/LegendAITypes.js.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts +96 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js +56 -0
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js.map +1 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.d.ts.map +1 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.js +6 -0
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.js.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisPanel.d.ts +24 -0
- package/lib/legend-ai/components/LegendAIAnalysisPanel.d.ts.map +1 -0
- package/lib/legend-ai/components/LegendAIAnalysisPanel.js +35 -0
- package/lib/legend-ai/components/LegendAIAnalysisPanel.js.map +1 -0
- package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts +23 -0
- package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts.map +1 -0
- package/lib/legend-ai/components/LegendAIAnalysisUtils.js +168 -0
- package/lib/legend-ai/components/LegendAIAnalysisUtils.js.map +1 -0
- package/lib/legend-ai/components/LegendAICharts.d.ts +25 -0
- package/lib/legend-ai/components/LegendAICharts.d.ts.map +1 -0
- package/lib/legend-ai/components/LegendAICharts.js +70 -0
- package/lib/legend-ai/components/LegendAICharts.js.map +1 -0
- package/lib/legend-ai/components/LegendAIChat.d.ts +2 -1
- package/lib/legend-ai/components/LegendAIChat.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIChat.js +14 -10
- package/lib/legend-ai/components/LegendAIChat.js.map +1 -1
- package/lib/legend-ai/index.d.ts +5 -2
- package/lib/legend-ai/index.d.ts.map +1 -1
- package/lib/legend-ai/index.js +5 -2
- package/lib/legend-ai/index.js.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatState.d.ts +12 -5
- package/lib/legend-ai/stores/LegendAIChatState.d.ts.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatState.js +604 -69
- package/lib/legend-ai/stores/LegendAIChatState.js.map +1 -1
- package/package.json +5 -5
- package/src/legend-ai/LegendAITypes.ts +51 -1
- package/src/legend-ai/LegendAI_LegendApplicationPlugin_Extension.ts +169 -0
- package/src/legend-ai/__test-utils__/LegendAITestUtils.ts +9 -0
- package/src/legend-ai/components/LegendAIAnalysisPanel.tsx +102 -0
- package/src/legend-ai/components/LegendAIAnalysisUtils.ts +226 -0
- package/src/legend-ai/components/LegendAICharts.tsx +166 -0
- package/src/legend-ai/components/LegendAIChat.tsx +74 -26
- package/src/legend-ai/index.ts +18 -0
- package/src/legend-ai/stores/LegendAIChatState.ts +1039 -128
- package/tsconfig.json +3 -0
|
@@ -15,12 +15,30 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
17
17
|
import { assertErrorThrown, noop, uuid } from '@finos/legend-shared';
|
|
18
|
-
import { LegendAIQuestionIntent, LegendAIThinkingStepStatus, LegendAIMessageRole, TDSServiceSourceType, buildColumnDefsFromNames, } from '../LegendAITypes.js';
|
|
18
|
+
import { LegendAIQuestionIntent, LegendAIThinkingStepStatus, LegendAIMessageRole, LegendAIErrorType, LegendAIServiceError, TDSServiceSourceType, buildColumnDefsFromNames, LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID, } from '../LegendAITypes.js';
|
|
19
19
|
import { LegendAIJudgeVerdict, } from '../LegendAI_LegendApplicationPlugin_Extension.js';
|
|
20
20
|
const MAX_ERROR_MESSAGE_LENGTH = 500;
|
|
21
21
|
const MAX_THINKING_ERROR_PREVIEW_LENGTH = 200;
|
|
22
22
|
const DEFAULT_MAX_JUDGE_ATTEMPTS = 5;
|
|
23
|
+
const DEFAULT_MAX_EXECUTION_RETRIES = 3;
|
|
24
|
+
const ANALYSIS_TIMEOUT_MS = 15_000;
|
|
23
25
|
const SUGGESTED_QUERIES_DELIMITER = '---SUGGESTED_QUERIES---';
|
|
26
|
+
export function elapsedSeconds(startTime, decimals = 1) {
|
|
27
|
+
return ((Date.now() - startTime) / 1000).toFixed(decimals);
|
|
28
|
+
}
|
|
29
|
+
function withTimeout(promise, ms) {
|
|
30
|
+
let timer;
|
|
31
|
+
return Promise.race([
|
|
32
|
+
promise.finally(() => {
|
|
33
|
+
if (timer !== undefined) {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
}
|
|
36
|
+
}),
|
|
37
|
+
new Promise((resolve) => {
|
|
38
|
+
timer = setTimeout(() => resolve(undefined), ms);
|
|
39
|
+
}),
|
|
40
|
+
]);
|
|
41
|
+
}
|
|
24
42
|
function deduplicateColumns(columns) {
|
|
25
43
|
const seen = new Map();
|
|
26
44
|
return columns.map((col) => {
|
|
@@ -29,7 +47,7 @@ function deduplicateColumns(columns) {
|
|
|
29
47
|
return count === 0 ? col : `${col}_${count + 1}`;
|
|
30
48
|
});
|
|
31
49
|
}
|
|
32
|
-
function createMessagePair(text) {
|
|
50
|
+
export function createMessagePair(text) {
|
|
33
51
|
return [
|
|
34
52
|
{ id: uuid(), role: LegendAIMessageRole.USER, text },
|
|
35
53
|
{
|
|
@@ -38,14 +56,17 @@ function createMessagePair(text) {
|
|
|
38
56
|
thinkingSteps: [],
|
|
39
57
|
sql: null,
|
|
40
58
|
textAnswer: null,
|
|
59
|
+
dataContext: null,
|
|
41
60
|
gridData: null,
|
|
42
61
|
error: null,
|
|
62
|
+
errorType: null,
|
|
43
63
|
sqlGenTime: null,
|
|
44
64
|
execTime: null,
|
|
45
65
|
thinkingDuration: null,
|
|
46
66
|
isProcessing: true,
|
|
47
67
|
isExecuting: false,
|
|
48
68
|
suggestedQueries: [],
|
|
69
|
+
fallbackAction: null,
|
|
49
70
|
},
|
|
50
71
|
];
|
|
51
72
|
}
|
|
@@ -66,7 +87,7 @@ export function addThinkingStep(setMessages, label) {
|
|
|
66
87
|
...msg.thinkingSteps.map((s) => s.status === LegendAIThinkingStepStatus.ACTIVE
|
|
67
88
|
? { ...s, status: LegendAIThinkingStepStatus.DONE }
|
|
68
89
|
: s),
|
|
69
|
-
{ label, status: LegendAIThinkingStepStatus.ACTIVE },
|
|
90
|
+
{ id: uuid(), label, status: LegendAIThinkingStepStatus.ACTIVE },
|
|
70
91
|
],
|
|
71
92
|
}));
|
|
72
93
|
}
|
|
@@ -77,14 +98,21 @@ export function completeThinkingSteps(setMessages) {
|
|
|
77
98
|
: s),
|
|
78
99
|
}));
|
|
79
100
|
}
|
|
80
|
-
export function
|
|
101
|
+
export function classifyError(error) {
|
|
102
|
+
if (error instanceof LegendAIServiceError) {
|
|
103
|
+
return error.errorType;
|
|
104
|
+
}
|
|
105
|
+
return LegendAIErrorType.GENERAL;
|
|
106
|
+
}
|
|
107
|
+
export function finishWithThinkingError(setMessages, errorMsg, startTime, errorType) {
|
|
81
108
|
updateLastAssistant(setMessages, (msg) => ({
|
|
82
109
|
thinkingSteps: msg.thinkingSteps.map((s) => s.status === LegendAIThinkingStepStatus.ACTIVE
|
|
83
110
|
? { ...s, status: LegendAIThinkingStepStatus.ERROR }
|
|
84
111
|
: s),
|
|
85
112
|
error: errorMsg.slice(0, MAX_ERROR_MESSAGE_LENGTH),
|
|
113
|
+
errorType: errorType ?? null,
|
|
86
114
|
isProcessing: false,
|
|
87
|
-
thinkingDuration: (
|
|
115
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
88
116
|
}));
|
|
89
117
|
}
|
|
90
118
|
export function buildConversationHistory(messages) {
|
|
@@ -196,7 +224,7 @@ export async function handleMetadataQuestion(question, metadata, context, startT
|
|
|
196
224
|
textAnswer: answer,
|
|
197
225
|
suggestedQueries,
|
|
198
226
|
isProcessing: false,
|
|
199
|
-
thinkingDuration: (
|
|
227
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
200
228
|
}));
|
|
201
229
|
}
|
|
202
230
|
export async function generateAndJudgeSql(question, services, coordinates, context, startTime) {
|
|
@@ -208,12 +236,12 @@ export async function generateAndJudgeSql(question, services, coordinates, conte
|
|
|
208
236
|
const { sql: generatedSql, failure, suggestion, } = plugin.extractSqlFromResponse(answerText);
|
|
209
237
|
if (failure) {
|
|
210
238
|
addThinkingStep(setMessages, `Generation failed: ${failure}`);
|
|
211
|
-
finishWithThinkingError(setMessages, buildGenerationFailureMessage(failure, suggestion, services), startTime);
|
|
239
|
+
finishWithThinkingError(setMessages, buildGenerationFailureMessage(failure, suggestion, services), startTime, LegendAIErrorType.GENERATION);
|
|
212
240
|
return null;
|
|
213
241
|
}
|
|
214
242
|
if (!generatedSql) {
|
|
215
243
|
addThinkingStep(setMessages, 'Could not extract SQL from response');
|
|
216
|
-
finishWithThinkingError(setMessages, 'Could not extract SQL from LLM response.\nTry rephrasing your question or ask about a specific service.', startTime);
|
|
244
|
+
finishWithThinkingError(setMessages, 'Could not extract SQL from LLM response.\nTry rephrasing your question or ask about a specific service.', startTime, LegendAIErrorType.GENERATION);
|
|
217
245
|
return null;
|
|
218
246
|
}
|
|
219
247
|
const maxAttempts = config.maxJudgeAttempts ?? DEFAULT_MAX_JUDGE_ATTEMPTS;
|
|
@@ -251,10 +279,10 @@ function reportExecutionResult(rawResult, setMessages, execStartTime, startTime)
|
|
|
251
279
|
completeThinkingSteps(setMessages);
|
|
252
280
|
updateLastAssistant(setMessages, () => ({
|
|
253
281
|
gridData: { columnDefs: buildColumnDefsFromNames(columns), rowData: rows },
|
|
254
|
-
execTime: (
|
|
282
|
+
execTime: elapsedSeconds(execStartTime, 2),
|
|
255
283
|
isProcessing: false,
|
|
256
284
|
isExecuting: false,
|
|
257
|
-
thinkingDuration: (
|
|
285
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
258
286
|
}));
|
|
259
287
|
return { columns, rows };
|
|
260
288
|
}
|
|
@@ -269,10 +297,13 @@ export async function executeSqlAndReport(sql, services, config, plugin, setMess
|
|
|
269
297
|
}
|
|
270
298
|
catch (executeError) {
|
|
271
299
|
assertErrorThrown(executeError);
|
|
300
|
+
const execErrorType = classifyError(executeError);
|
|
272
301
|
addThinkingStep(setMessages, `Execution failed: ${executeError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
273
|
-
finishWithThinkingError(setMessages, buildExecutionErrorMessage(executeError.message, services), startTime
|
|
302
|
+
finishWithThinkingError(setMessages, buildExecutionErrorMessage(executeError.message, services), startTime, execErrorType === LegendAIErrorType.GENERAL
|
|
303
|
+
? LegendAIErrorType.EXECUTION
|
|
304
|
+
: execErrorType);
|
|
274
305
|
updateLastAssistant(setMessages, () => ({
|
|
275
|
-
execTime: (
|
|
306
|
+
execTime: elapsedSeconds(execStartTime, 2),
|
|
276
307
|
isExecuting: false,
|
|
277
308
|
}));
|
|
278
309
|
return undefined;
|
|
@@ -287,25 +318,100 @@ export async function executePureQueryAndReport(pureQuery, pureExecutionContext,
|
|
|
287
318
|
}
|
|
288
319
|
catch (executeError) {
|
|
289
320
|
assertErrorThrown(executeError);
|
|
321
|
+
const execErrorType = classifyError(executeError);
|
|
290
322
|
addThinkingStep(setMessages, `Execution failed: ${executeError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
291
323
|
completeThinkingSteps(setMessages);
|
|
292
324
|
updateLastAssistant(setMessages, () => ({
|
|
293
|
-
execTime: (
|
|
325
|
+
execTime: elapsedSeconds(execStartTime, 2),
|
|
294
326
|
isExecuting: false,
|
|
295
327
|
isProcessing: false,
|
|
296
328
|
error: `Execution failed: ${executeError.message.slice(0, MAX_ERROR_MESSAGE_LENGTH)}`,
|
|
297
|
-
|
|
329
|
+
errorType: execErrorType === LegendAIErrorType.GENERAL
|
|
330
|
+
? LegendAIErrorType.EXECUTION
|
|
331
|
+
: execErrorType,
|
|
332
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
298
333
|
}));
|
|
299
334
|
return { columns: [], rows: [] };
|
|
300
335
|
}
|
|
301
336
|
}
|
|
302
|
-
export async function
|
|
337
|
+
export async function analyzeOrchestratorResults(question, query, execResult, metadata, context, startTime) {
|
|
338
|
+
const { config, plugin, setMessages } = context;
|
|
339
|
+
addThinkingStep(setMessages, 'Analyzing results...');
|
|
340
|
+
updateLastAssistant(setMessages, () => ({
|
|
341
|
+
isProcessing: true,
|
|
342
|
+
}));
|
|
343
|
+
const analysis = await withTimeout(plugin.analyzeQueryResults(question, query, execResult.columns, execResult.rows, metadata, config), ANALYSIS_TIMEOUT_MS);
|
|
344
|
+
if (analysis) {
|
|
345
|
+
completeThinkingSteps(setMessages);
|
|
346
|
+
updateLastAssistant(setMessages, () => ({
|
|
347
|
+
textAnswer: analysis.summary,
|
|
348
|
+
suggestedQueries: analysis.suggestedQueries,
|
|
349
|
+
isProcessing: false,
|
|
350
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
351
|
+
}));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
async function handleEmptyOrchestratorResults(question, legendQuery, orchestratorOptions, metadata, resolvedEntities, context, startTime) {
|
|
355
|
+
const { dataProductCoordinates, pureExecutionContext } = orchestratorOptions;
|
|
356
|
+
const { config, plugin, setMessages } = context;
|
|
357
|
+
if (resolvedEntities.relatedEntities.length > 0) {
|
|
358
|
+
const alternateRoot = resolvedEntities.relatedEntities[0];
|
|
359
|
+
if (alternateRoot) {
|
|
360
|
+
addThinkingStep(setMessages, `No results with ${resolvedEntities.rootEntity.split('::').pop() ?? resolvedEntities.rootEntity}, retrying with ${alternateRoot.split('::').pop() ?? alternateRoot}...`);
|
|
361
|
+
try {
|
|
362
|
+
const retryResponse = await plugin.generateQueryViaOrchestrator({
|
|
363
|
+
user_question: question,
|
|
364
|
+
semantic_search_resolution_details: {
|
|
365
|
+
data_product_coordinates: dataProductCoordinates,
|
|
366
|
+
root_entity: alternateRoot,
|
|
367
|
+
related_entities: resolvedEntities.relatedEntities.slice(1),
|
|
368
|
+
},
|
|
369
|
+
}, config);
|
|
370
|
+
updateLastAssistant(setMessages, () => ({
|
|
371
|
+
sql: retryResponse.legend_query,
|
|
372
|
+
sqlGenTime: elapsedSeconds(startTime, 2),
|
|
373
|
+
isExecuting: true,
|
|
374
|
+
}));
|
|
375
|
+
const retryResult = await executePureQueryAndReport(retryResponse.legend_query, pureExecutionContext, dataProductCoordinates, config, plugin, setMessages, startTime);
|
|
376
|
+
if (retryResult.rows.length > 0) {
|
|
377
|
+
await analyzeOrchestratorResults(question, retryResponse.legend_query, retryResult, metadata, context, startTime);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
/* empty */
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
addThinkingStep(setMessages, 'No results returned \u2014 building contextual guidance...');
|
|
387
|
+
updateLastAssistant(setMessages, () => ({
|
|
388
|
+
isProcessing: true,
|
|
389
|
+
}));
|
|
390
|
+
const fallback = await withTimeout(plugin.buildNoResultsFallback(question, legendQuery, metadata, config), ANALYSIS_TIMEOUT_MS);
|
|
391
|
+
if (fallback) {
|
|
392
|
+
completeThinkingSteps(setMessages);
|
|
393
|
+
updateLastAssistant(setMessages, () => ({
|
|
394
|
+
textAnswer: fallback.summary,
|
|
395
|
+
suggestedQueries: fallback.suggestedQueries,
|
|
396
|
+
isProcessing: false,
|
|
397
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
398
|
+
}));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
export async function processQuestionViaOrchestrator(question, dataProductCoordinates, metadata, context, pureExecutionContext, preResolvedEntities) {
|
|
303
402
|
const { config, plugin, setMessages } = context;
|
|
304
403
|
const startTime = Date.now();
|
|
305
404
|
try {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
405
|
+
let resolvedEntities;
|
|
406
|
+
if (preResolvedEntities) {
|
|
407
|
+
resolvedEntities = preResolvedEntities;
|
|
408
|
+
addThinkingStep(setMessages, `Using pre-resolved root entity: ${resolvedEntities.rootEntity.split('::').pop() ?? resolvedEntities.rootEntity}`);
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
addThinkingStep(setMessages, 'Resolving entities for your query...');
|
|
412
|
+
resolvedEntities = await plugin.resolveEntitiesForQuery(question, dataProductCoordinates, config, pureExecutionContext);
|
|
413
|
+
addThinkingStep(setMessages, `Found root entity: ${resolvedEntities.rootEntity.split('::').pop() ?? resolvedEntities.rootEntity}`);
|
|
414
|
+
}
|
|
309
415
|
if (resolvedEntities.relatedEntities.length > 0) {
|
|
310
416
|
addThinkingStep(setMessages, `Found ${resolvedEntities.relatedEntities.length} related entities`);
|
|
311
417
|
}
|
|
@@ -318,7 +424,7 @@ export async function processQuestionViaOrchestrator(question, dataProductCoordi
|
|
|
318
424
|
related_entities: resolvedEntities.relatedEntities,
|
|
319
425
|
},
|
|
320
426
|
}, config);
|
|
321
|
-
const queryGenTime = (
|
|
427
|
+
const queryGenTime = elapsedSeconds(startTime, 2);
|
|
322
428
|
completeThinkingSteps(setMessages);
|
|
323
429
|
updateLastAssistant(setMessages, () => ({
|
|
324
430
|
sql: orchestratorResponse.legend_query,
|
|
@@ -331,98 +437,465 @@ export async function processQuestionViaOrchestrator(question, dataProductCoordi
|
|
|
331
437
|
isProcessing: false,
|
|
332
438
|
isExecuting: false,
|
|
333
439
|
error: 'No execution context available — cannot execute query via engine.',
|
|
334
|
-
|
|
440
|
+
errorType: LegendAIErrorType.EXECUTION,
|
|
441
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
335
442
|
}));
|
|
336
443
|
return;
|
|
337
444
|
}
|
|
338
|
-
await executePureQueryAndReport(orchestratorResponse.legend_query, pureExecutionContext, dataProductCoordinates, config, plugin, setMessages, startTime);
|
|
445
|
+
const execResult = await executePureQueryAndReport(orchestratorResponse.legend_query, pureExecutionContext, dataProductCoordinates, config, plugin, setMessages, startTime);
|
|
446
|
+
try {
|
|
447
|
+
if (execResult.rows.length > 0) {
|
|
448
|
+
await analyzeOrchestratorResults(question, orchestratorResponse.legend_query, execResult, metadata, context, startTime);
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
await handleEmptyOrchestratorResults(question, orchestratorResponse.legend_query, { dataProductCoordinates, pureExecutionContext }, metadata, resolvedEntities, context, startTime);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
catch {
|
|
455
|
+
/* empty */
|
|
456
|
+
}
|
|
457
|
+
finally {
|
|
458
|
+
completeThinkingSteps(setMessages);
|
|
459
|
+
updateLastAssistant(setMessages, () => ({
|
|
460
|
+
isProcessing: false,
|
|
461
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
462
|
+
}));
|
|
463
|
+
}
|
|
339
464
|
}
|
|
340
465
|
catch (error) {
|
|
341
466
|
assertErrorThrown(error);
|
|
467
|
+
const orchErrorType = classifyError(error);
|
|
342
468
|
addThinkingStep(setMessages, `Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
343
|
-
|
|
469
|
+
try {
|
|
470
|
+
addThinkingStep(setMessages, 'Building guidance from available metadata...');
|
|
471
|
+
const fallbackText = await withTimeout(plugin.buildFailureFallback(question, error.message, metadata, config), ANALYSIS_TIMEOUT_MS);
|
|
472
|
+
if (fallbackText) {
|
|
473
|
+
completeThinkingSteps(setMessages);
|
|
474
|
+
updateLastAssistant(setMessages, () => ({
|
|
475
|
+
dataContext: fallbackText,
|
|
476
|
+
isProcessing: false,
|
|
477
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
478
|
+
}));
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
/* empty */
|
|
484
|
+
}
|
|
485
|
+
finishWithThinkingError(setMessages, error.message, startTime, orchErrorType);
|
|
344
486
|
}
|
|
345
487
|
}
|
|
488
|
+
function cleanLlmSqlResponse(raw) {
|
|
489
|
+
return raw
|
|
490
|
+
.trim()
|
|
491
|
+
.replace(/^```\w*\n?/, '')
|
|
492
|
+
.replace(/\n?```$/, '')
|
|
493
|
+
.replace(/;\s*$/, '')
|
|
494
|
+
.trim();
|
|
495
|
+
}
|
|
496
|
+
function isValidSqlCorrection(trimmed, currentSql) {
|
|
497
|
+
return (trimmed.length > 0 &&
|
|
498
|
+
trimmed.toLowerCase().startsWith('select') &&
|
|
499
|
+
trimmed !== currentSql);
|
|
500
|
+
}
|
|
501
|
+
const ALIAS_DOT_COL_PATTERN = /\b(?<tbl>[a-z]\w*)\s*\.\s*"(?<col>[^"]+)"/gi;
|
|
502
|
+
const JOIN_PATTERN = /\bJOIN\b/i;
|
|
503
|
+
const ORDER_BY_SPLIT = /\bORDER\s+BY\b/i;
|
|
504
|
+
export function sanitizeJoinOrderBy(sql) {
|
|
505
|
+
if (!JOIN_PATTERN.test(sql)) {
|
|
506
|
+
return sql;
|
|
507
|
+
}
|
|
508
|
+
const parts = sql.split(ORDER_BY_SPLIT);
|
|
509
|
+
if (parts.length < 2) {
|
|
510
|
+
return sql;
|
|
511
|
+
}
|
|
512
|
+
const beforeOrderBy = parts[0] ?? '';
|
|
513
|
+
const afterOrderBy = parts.slice(1).join('ORDER BY').replace(/^\s+/, '');
|
|
514
|
+
const selectAliases = new Map();
|
|
515
|
+
const aliasRegex = /\b(?<tbl>[a-z]\w*)\s*\.\s*"(?<col>[^"]+)"\s+AS\s+(?:"(?<qAlias>[^"]+)"|(?<uAlias>\w+))/gi;
|
|
516
|
+
let m;
|
|
517
|
+
while ((m = aliasRegex.exec(beforeOrderBy)) !== null) {
|
|
518
|
+
const tableAlias = (m.groups?.tbl ?? '').toLowerCase();
|
|
519
|
+
const colName = (m.groups?.col ?? '').toLowerCase();
|
|
520
|
+
const asAlias = m.groups?.qAlias ?? m.groups?.uAlias ?? '';
|
|
521
|
+
selectAliases.set(`${tableAlias}.${colName}`, asAlias);
|
|
522
|
+
}
|
|
523
|
+
if (selectAliases.size === 0) {
|
|
524
|
+
return sql;
|
|
525
|
+
}
|
|
526
|
+
const rewritten = afterOrderBy.replaceAll(ALIAS_DOT_COL_PATTERN, (...args) => {
|
|
527
|
+
const groups = args[args.length - 1];
|
|
528
|
+
const key = `${groups.tbl.toLowerCase()}.${groups.col.toLowerCase()}`;
|
|
529
|
+
const alias = selectAliases.get(key);
|
|
530
|
+
return alias ? `"${alias}"` : String(args[0]);
|
|
531
|
+
});
|
|
532
|
+
if (rewritten === afterOrderBy) {
|
|
533
|
+
return sql;
|
|
534
|
+
}
|
|
535
|
+
return `${beforeOrderBy}ORDER BY ${rewritten}`;
|
|
536
|
+
}
|
|
537
|
+
const UNION_ALL_PATTERN = /\bUNION\s+ALL\b/i;
|
|
538
|
+
const LITERAL_COL_PATTERN = /,\s*'[^']*'\s+AS\s+(?:"[^"]+"|[a-z]\w*)/gi;
|
|
539
|
+
export function sanitizeLiteralColumns(sql) {
|
|
540
|
+
if (!UNION_ALL_PATTERN.test(sql)) {
|
|
541
|
+
return sql;
|
|
542
|
+
}
|
|
543
|
+
LITERAL_COL_PATTERN.lastIndex = 0;
|
|
544
|
+
if (!LITERAL_COL_PATTERN.test(sql)) {
|
|
545
|
+
return sql;
|
|
546
|
+
}
|
|
547
|
+
LITERAL_COL_PATTERN.lastIndex = 0;
|
|
548
|
+
return sql.replace(LITERAL_COL_PATTERN, '');
|
|
549
|
+
}
|
|
550
|
+
const SERVICE_PARAM_DATE_LIKE = /date|time|day|month|year|period|asOf|businessDate|processingDate|snapshot/i;
|
|
551
|
+
function hasUnresolvableParams(service) {
|
|
552
|
+
return service.parameters.some((p) => !SERVICE_PARAM_DATE_LIKE.test(p));
|
|
553
|
+
}
|
|
554
|
+
function getNonDateParamNames(service) {
|
|
555
|
+
return service.parameters.filter((p) => !SERVICE_PARAM_DATE_LIKE.test(p));
|
|
556
|
+
}
|
|
557
|
+
export function stripNonDateServiceParams(sql) {
|
|
558
|
+
return sql.replaceAll(/,\s*\w+\s*=>\s*'[^']*'/g, (match) => {
|
|
559
|
+
const paramName = /,\s*(?<param>\w+)\s*=>/.exec(match)?.groups?.param;
|
|
560
|
+
if (!paramName) {
|
|
561
|
+
return match;
|
|
562
|
+
}
|
|
563
|
+
if (paramName === 'coordinates' ||
|
|
564
|
+
SERVICE_PARAM_DATE_LIKE.test(paramName)) {
|
|
565
|
+
return match;
|
|
566
|
+
}
|
|
567
|
+
return '';
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
async function executeSqlForServices(sql, services, dataProductCoordinates, plugin, config) {
|
|
571
|
+
const safeSql = sanitizeLiteralColumns(sanitizeJoinOrderBy(sql));
|
|
572
|
+
const isAccessPoint = services.some((s) => s.sourceType === TDSServiceSourceType.ACCESS_POINT);
|
|
573
|
+
if (isAccessPoint && dataProductCoordinates) {
|
|
574
|
+
return plugin.executeLakehouseSql(safeSql, dataProductCoordinates, config);
|
|
575
|
+
}
|
|
576
|
+
return plugin.executeSql(safeSql, config);
|
|
577
|
+
}
|
|
578
|
+
async function executeSqlWithRetries(initialSql, question, services, coordinates, dataProductCoordinates, context) {
|
|
579
|
+
const { plugin, config, setMessages } = context;
|
|
580
|
+
let currentSql = initialSql;
|
|
581
|
+
for (let attempt = 0; attempt <= DEFAULT_MAX_EXECUTION_RETRIES; attempt++) {
|
|
582
|
+
try {
|
|
583
|
+
const result = await executeSqlForServices(currentSql, services, dataProductCoordinates, plugin, config);
|
|
584
|
+
return { sql: currentSql, result };
|
|
585
|
+
}
|
|
586
|
+
catch (executeError) {
|
|
587
|
+
assertErrorThrown(executeError);
|
|
588
|
+
if (attempt >= DEFAULT_MAX_EXECUTION_RETRIES) {
|
|
589
|
+
return { sql: currentSql, error: executeError.message };
|
|
590
|
+
}
|
|
591
|
+
addThinkingStep(setMessages, `Execution failed (attempt ${attempt + 1}/${DEFAULT_MAX_EXECUTION_RETRIES + 1}), correcting query...`);
|
|
592
|
+
const corrected = await attemptErrorCorrection(currentSql, executeError.message, question, services, coordinates, plugin, config);
|
|
593
|
+
if (corrected) {
|
|
594
|
+
currentSql = corrected;
|
|
595
|
+
updateLastAssistant(setMessages, () => ({ sql: currentSql }));
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
return { sql: currentSql, error: executeError.message };
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return { sql: currentSql };
|
|
602
|
+
}
|
|
603
|
+
async function attemptErrorCorrection(currentSql, errorMessage, question, services, coordinates, plugin, config) {
|
|
604
|
+
const prompt = plugin.buildErrorCorrectionPrompt(currentSql, errorMessage, question, services, coordinates);
|
|
605
|
+
if (!prompt) {
|
|
606
|
+
return undefined;
|
|
607
|
+
}
|
|
608
|
+
try {
|
|
609
|
+
const correctedSql = await plugin.callLLM(prompt, config);
|
|
610
|
+
const trimmed = cleanLlmSqlResponse(correctedSql);
|
|
611
|
+
if (isValidSqlCorrection(trimmed, currentSql)) {
|
|
612
|
+
return trimmed;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
catch {
|
|
616
|
+
/* empty */
|
|
617
|
+
}
|
|
618
|
+
return undefined;
|
|
619
|
+
}
|
|
620
|
+
async function attemptZeroRowCorrection(currentSql, question, services, coordinates, dataProductCoordinates, context) {
|
|
621
|
+
const { plugin, config, setMessages } = context;
|
|
622
|
+
addThinkingStep(setMessages, 'Query returned 0 rows, attempting filter correction...');
|
|
623
|
+
const prompt = plugin.buildZeroRowCorrectionPrompt(currentSql, question, services, coordinates);
|
|
624
|
+
if (!prompt) {
|
|
625
|
+
return undefined;
|
|
626
|
+
}
|
|
627
|
+
try {
|
|
628
|
+
const correctedSql = await plugin.callLLM(prompt, config);
|
|
629
|
+
const trimmed = cleanLlmSqlResponse(correctedSql);
|
|
630
|
+
if (!isValidSqlCorrection(trimmed, currentSql)) {
|
|
631
|
+
return undefined;
|
|
632
|
+
}
|
|
633
|
+
addThinkingStep(setMessages, 'Retrying with corrected filters...');
|
|
634
|
+
updateLastAssistant(setMessages, () => ({ sql: trimmed }));
|
|
635
|
+
try {
|
|
636
|
+
const retryResult = await executeSqlForServices(trimmed, services, dataProductCoordinates, plugin, config);
|
|
637
|
+
if (retryResult.rows.length > 0) {
|
|
638
|
+
return { sql: trimmed, result: retryResult };
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
catch {
|
|
642
|
+
/* empty */
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
/* empty */
|
|
647
|
+
}
|
|
648
|
+
return undefined;
|
|
649
|
+
}
|
|
650
|
+
function buildZeroRowMessage(services) {
|
|
651
|
+
const withUnresolvable = services.filter((s) => hasUnresolvableParams(s));
|
|
652
|
+
if (withUnresolvable.length > 0) {
|
|
653
|
+
const parts = [];
|
|
654
|
+
for (const svc of withUnresolvable) {
|
|
655
|
+
for (const paramName of getNonDateParamNames(svc)) {
|
|
656
|
+
const matchingCol = svc.columns.find((c) => c.name === paramName);
|
|
657
|
+
const docHint = matchingCol?.documentation ?? matchingCol?.sampleValues;
|
|
658
|
+
if (docHint) {
|
|
659
|
+
parts.push(`**${paramName}** (${docHint})`);
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
parts.push(`**${paramName}**`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const uniqueParts = [...new Set(parts)];
|
|
667
|
+
const firstSvc = withUnresolvable[0];
|
|
668
|
+
const firstParam = firstSvc
|
|
669
|
+
? (getNonDateParamNames(firstSvc)[0] ?? 'parameter')
|
|
670
|
+
: 'parameter';
|
|
671
|
+
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]".`;
|
|
672
|
+
}
|
|
673
|
+
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.';
|
|
674
|
+
}
|
|
675
|
+
function offerOrchestratorFallbackMessage(setMessages, startTime, fallbackMessage) {
|
|
676
|
+
updateLastAssistant(setMessages, () => ({
|
|
677
|
+
textAnswer: fallbackMessage,
|
|
678
|
+
fallbackAction: {
|
|
679
|
+
label: 'Try Legend AI Orchestrator',
|
|
680
|
+
actionId: LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
|
|
681
|
+
},
|
|
682
|
+
isProcessing: false,
|
|
683
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
684
|
+
}));
|
|
685
|
+
}
|
|
686
|
+
function reportFatalQueryError(setMessages, startTime, errorMessage, errorType) {
|
|
687
|
+
finishWithThinkingError(setMessages, errorMessage, startTime, errorType);
|
|
688
|
+
}
|
|
689
|
+
function handleSqlGenerationFailure(setMessages, startTime, hasOrchestratorFallback, orchestratorMessage, errorMessage, errorType) {
|
|
690
|
+
completeThinkingSteps(setMessages);
|
|
691
|
+
if (hasOrchestratorFallback) {
|
|
692
|
+
offerOrchestratorFallbackMessage(setMessages, startTime, orchestratorMessage);
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
reportFatalQueryError(setMessages, startTime, errorMessage, errorType);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
async function reportQueryResults(report, metadata, context, startTime, hasOrchestratorFallback) {
|
|
699
|
+
const { currentSql, sqlResult, question, services } = report;
|
|
700
|
+
const { setMessages } = context;
|
|
701
|
+
if (sqlResult.rows.length > 0) {
|
|
702
|
+
const columns = deduplicateColumns(sqlResult.columns);
|
|
703
|
+
const rows = sqlResult.rows;
|
|
704
|
+
completeThinkingSteps(setMessages);
|
|
705
|
+
addThinkingStep(setMessages, `Retrieved ${rows.length} row${rows.length === 1 ? '' : 's'}`);
|
|
706
|
+
completeThinkingSteps(setMessages);
|
|
707
|
+
updateLastAssistant(setMessages, () => ({
|
|
708
|
+
sql: currentSql,
|
|
709
|
+
gridData: {
|
|
710
|
+
columnDefs: buildColumnDefsFromNames(columns),
|
|
711
|
+
rowData: rows,
|
|
712
|
+
},
|
|
713
|
+
execTime: elapsedSeconds(startTime, 2),
|
|
714
|
+
isProcessing: true,
|
|
715
|
+
isExecuting: false,
|
|
716
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
717
|
+
}));
|
|
718
|
+
try {
|
|
719
|
+
await analyzeOrchestratorResults(question, currentSql, sqlResult, metadata, context, startTime);
|
|
720
|
+
}
|
|
721
|
+
catch {
|
|
722
|
+
/* empty */
|
|
723
|
+
}
|
|
724
|
+
finally {
|
|
725
|
+
completeThinkingSteps(setMessages);
|
|
726
|
+
updateLastAssistant(setMessages, () => ({
|
|
727
|
+
isProcessing: false,
|
|
728
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
729
|
+
}));
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
else {
|
|
733
|
+
addThinkingStep(setMessages, 'Query returned 0 rows after correction attempts.');
|
|
734
|
+
completeThinkingSteps(setMessages);
|
|
735
|
+
const fallback = hasOrchestratorFallback
|
|
736
|
+
? {
|
|
737
|
+
fallbackAction: {
|
|
738
|
+
label: 'Try Legend AI Orchestrator',
|
|
739
|
+
actionId: LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
|
|
740
|
+
},
|
|
741
|
+
}
|
|
742
|
+
: {};
|
|
743
|
+
updateLastAssistant(setMessages, () => ({
|
|
744
|
+
textAnswer: buildZeroRowMessage(services),
|
|
745
|
+
...fallback,
|
|
746
|
+
isProcessing: false,
|
|
747
|
+
isExecuting: false,
|
|
748
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
749
|
+
}));
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
async function selectBestServices(question, services, context) {
|
|
753
|
+
const { plugin, config, setMessages } = context;
|
|
754
|
+
if (services.length <= 1) {
|
|
755
|
+
return services;
|
|
756
|
+
}
|
|
757
|
+
try {
|
|
758
|
+
addThinkingStep(setMessages, 'Selecting best service for your query...');
|
|
759
|
+
return await plugin.selectRelevantServices(question, services, config);
|
|
760
|
+
}
|
|
761
|
+
catch {
|
|
762
|
+
return services;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
async function tryRecoverZeroRows(currentSql, sqlResult, question, selectedServices, coordinates, dataProductCoordinates, context) {
|
|
766
|
+
const { plugin, config, setMessages } = context;
|
|
767
|
+
let recoveredSql = currentSql;
|
|
768
|
+
let recoveredResult = sqlResult;
|
|
769
|
+
const strippedSql = stripNonDateServiceParams(recoveredSql);
|
|
770
|
+
if (strippedSql !== recoveredSql) {
|
|
771
|
+
addThinkingStep(setMessages, 'Trying query without guessed parameter values...');
|
|
772
|
+
try {
|
|
773
|
+
const strippedResult = await executeSqlForServices(strippedSql, selectedServices, dataProductCoordinates, plugin, config);
|
|
774
|
+
if (strippedResult.rows.length > 0) {
|
|
775
|
+
recoveredSql = strippedSql;
|
|
776
|
+
recoveredResult = strippedResult;
|
|
777
|
+
updateLastAssistant(setMessages, () => ({ sql: strippedSql }));
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
catch {
|
|
781
|
+
/* empty */
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (recoveredResult.rows.length === 0) {
|
|
785
|
+
const correction = await attemptZeroRowCorrection(recoveredSql, question, selectedServices, coordinates, dataProductCoordinates, context);
|
|
786
|
+
if (correction) {
|
|
787
|
+
recoveredSql = correction.sql;
|
|
788
|
+
recoveredResult = correction.result;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return { sql: recoveredSql, result: recoveredResult };
|
|
792
|
+
}
|
|
346
793
|
async function processDataQuery(question, services, coordinates, metadata, context, startTime, orchestratorOptions) {
|
|
347
|
-
const { config,
|
|
794
|
+
const { config, setMessages } = context;
|
|
348
795
|
const dataProductCoordinates = orchestratorOptions?.dataProductCoordinates;
|
|
349
|
-
const
|
|
796
|
+
const hasOrchestratorFallback = Boolean(config.orchestratorUrl && dataProductCoordinates);
|
|
350
797
|
if (services.length === 0) {
|
|
351
|
-
|
|
352
|
-
completeThinkingSteps(setMessages);
|
|
353
|
-
await processQuestionViaOrchestrator(question, dataProductCoordinates, metadata, context, pureExecutionContext);
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
finishWithThinkingError(setMessages, 'No TDS services available for querying', startTime);
|
|
798
|
+
handleSqlGenerationFailure(setMessages, startTime, hasOrchestratorFallback, 'No TDS services available for SQL querying. You can try the Legend AI Orchestrator to generate a Pure query instead.', 'No TDS services available for querying', LegendAIErrorType.GENERAL);
|
|
357
799
|
return;
|
|
358
800
|
}
|
|
359
801
|
addThinkingStep(setMessages, 'Found relevant services to query');
|
|
360
|
-
const
|
|
802
|
+
const selectedServices = await selectBestServices(question, services, context);
|
|
803
|
+
const judgedSql = await generateAndJudgeSql(question, selectedServices, coordinates, context, startTime);
|
|
361
804
|
if (!judgedSql) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
updateLastAssistant(setMessages, () => ({
|
|
365
|
-
error: null,
|
|
366
|
-
isProcessing: true,
|
|
367
|
-
}));
|
|
368
|
-
await processQuestionViaOrchestrator(question, dataProductCoordinates, metadata, context, pureExecutionContext);
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
805
|
+
addThinkingStep(setMessages, 'SQL generation could not produce a valid query.');
|
|
806
|
+
handleSqlGenerationFailure(setMessages, startTime, hasOrchestratorFallback, 'SQL generation could not handle this query. You can try the Legend AI Orchestrator to generate a Pure query instead.', 'SQL generation could not handle this query. Try rephrasing your question.', LegendAIErrorType.GENERATION);
|
|
371
807
|
return;
|
|
372
808
|
}
|
|
373
|
-
const sqlGenTimeValue = (
|
|
809
|
+
const sqlGenTimeValue = elapsedSeconds(startTime, 2);
|
|
374
810
|
completeThinkingSteps(setMessages);
|
|
375
811
|
updateLastAssistant(setMessages, () => ({
|
|
376
812
|
sql: judgedSql,
|
|
377
813
|
sqlGenTime: sqlGenTimeValue,
|
|
378
814
|
isExecuting: true,
|
|
379
815
|
}));
|
|
380
|
-
const
|
|
381
|
-
if (
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
816
|
+
const execOutcome = await executeSqlWithRetries(judgedSql, question, selectedServices, coordinates, dataProductCoordinates, context);
|
|
817
|
+
if (execOutcome.error) {
|
|
818
|
+
const execErrorType = classifyError(new Error(execOutcome.error));
|
|
819
|
+
addThinkingStep(setMessages, `Execution failed: ${execOutcome.error.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
820
|
+
finishWithThinkingError(setMessages, buildExecutionErrorMessage(execOutcome.error, selectedServices), startTime, execErrorType === LegendAIErrorType.GENERAL
|
|
821
|
+
? LegendAIErrorType.EXECUTION
|
|
822
|
+
: execErrorType);
|
|
385
823
|
updateLastAssistant(setMessages, () => ({
|
|
386
|
-
gridData: null,
|
|
387
|
-
error: null,
|
|
388
|
-
isProcessing: true,
|
|
389
824
|
isExecuting: false,
|
|
825
|
+
...(hasOrchestratorFallback
|
|
826
|
+
? {
|
|
827
|
+
fallbackAction: {
|
|
828
|
+
label: 'Try Legend AI Orchestrator',
|
|
829
|
+
actionId: LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
|
|
830
|
+
},
|
|
831
|
+
}
|
|
832
|
+
: {}),
|
|
390
833
|
}));
|
|
391
|
-
|
|
834
|
+
return;
|
|
392
835
|
}
|
|
836
|
+
if (!execOutcome.result) {
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
let currentSql = execOutcome.sql;
|
|
840
|
+
let sqlResult = execOutcome.result;
|
|
841
|
+
if (sqlResult.rows.length === 0) {
|
|
842
|
+
const recovered = await tryRecoverZeroRows(currentSql, sqlResult, question, selectedServices, coordinates, dataProductCoordinates, context);
|
|
843
|
+
currentSql = recovered.sql;
|
|
844
|
+
sqlResult = recovered.result;
|
|
845
|
+
}
|
|
846
|
+
await reportQueryResults({
|
|
847
|
+
currentSql,
|
|
848
|
+
sqlResult,
|
|
849
|
+
question,
|
|
850
|
+
services: selectedServices,
|
|
851
|
+
}, metadata, context, startTime, hasOrchestratorFallback);
|
|
393
852
|
}
|
|
394
853
|
export async function processQuestion(question, services, coordinates, metadata, context, dataProductCoordinates, pureExecutionContext) {
|
|
395
854
|
const { config, plugin, setMessages } = context;
|
|
396
855
|
const startTime = Date.now();
|
|
397
856
|
try {
|
|
398
857
|
addThinkingStep(setMessages, 'Analyzing your question...');
|
|
399
|
-
const
|
|
400
|
-
const intent = await plugin.classifyQuestionIntent(question, services.length > 0, config, serviceNames);
|
|
401
|
-
if (intent === LegendAIQuestionIntent.METADATA) {
|
|
402
|
-
await handleMetadataQuestion(question, metadata, context, startTime, services.length > 0);
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
if (intent === LegendAIQuestionIntent.ORCHESTRATOR) {
|
|
406
|
-
if (config.orchestratorUrl && dataProductCoordinates) {
|
|
407
|
-
completeThinkingSteps(setMessages);
|
|
408
|
-
await processQuestionViaOrchestrator(question, dataProductCoordinates, metadata, context, pureExecutionContext);
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
addThinkingStep(setMessages, 'Orchestrator not available, trying SQL generation...');
|
|
412
|
-
}
|
|
413
|
-
await processDataQuery(question, services, coordinates, metadata, context, startTime, dataProductCoordinates
|
|
858
|
+
const orchestratorOpts = dataProductCoordinates
|
|
414
859
|
? {
|
|
415
860
|
dataProductCoordinates,
|
|
416
861
|
...(pureExecutionContext === undefined
|
|
417
862
|
? {}
|
|
418
863
|
: { pureExecutionContext }),
|
|
419
864
|
}
|
|
420
|
-
: undefined
|
|
865
|
+
: undefined;
|
|
866
|
+
if (services.length > 0) {
|
|
867
|
+
const serviceNames = services.map((s) => s.title);
|
|
868
|
+
const intent = await plugin.classifyQuestionIntent(question, true, config, serviceNames);
|
|
869
|
+
if (intent === LegendAIQuestionIntent.METADATA) {
|
|
870
|
+
await handleMetadataQuestion(question, metadata, context, startTime, true);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
// DATA_QUERY or ORCHESTRATOR — try SQL generation.
|
|
874
|
+
// If SQL throws, fall back to metadata as a safety net
|
|
875
|
+
// (e.g. misclassified metadata question).
|
|
876
|
+
try {
|
|
877
|
+
await processDataQuery(question, services, coordinates, metadata, context, startTime, orchestratorOpts);
|
|
878
|
+
}
|
|
879
|
+
catch (sqlError) {
|
|
880
|
+
assertErrorThrown(sqlError);
|
|
881
|
+
addThinkingStep(setMessages, 'SQL generation failed, answering from product metadata...');
|
|
882
|
+
await handleMetadataQuestion(question, metadata, context, startTime, true);
|
|
883
|
+
}
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
// No services available — use orchestrator if configured, else metadata only.
|
|
887
|
+
if (config.orchestratorUrl && dataProductCoordinates) {
|
|
888
|
+
completeThinkingSteps(setMessages);
|
|
889
|
+
await processQuestionViaOrchestrator(question, dataProductCoordinates, metadata, context, pureExecutionContext);
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
await handleMetadataQuestion(question, metadata, context, startTime, false);
|
|
893
|
+
}
|
|
421
894
|
}
|
|
422
895
|
catch (error) {
|
|
423
896
|
assertErrorThrown(error);
|
|
424
897
|
addThinkingStep(setMessages, `Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
425
|
-
finishWithThinkingError(setMessages, error.message, startTime);
|
|
898
|
+
finishWithThinkingError(setMessages, error.message, startTime, classifyError(error));
|
|
426
899
|
}
|
|
427
900
|
}
|
|
428
901
|
export async function processQuestionWithIntent(question, intent, services, coordinates, metadata, context, orchestratorOptions) {
|
|
@@ -431,11 +904,32 @@ export async function processQuestionWithIntent(question, intent, services, coor
|
|
|
431
904
|
const pureExecutionContext = orchestratorOptions?.pureExecutionContext;
|
|
432
905
|
if (intent === LegendAIQuestionIntent.METADATA) {
|
|
433
906
|
const startTime = Date.now();
|
|
434
|
-
|
|
907
|
+
try {
|
|
908
|
+
await handleMetadataQuestion(question, metadata, context, startTime, services.length > 0);
|
|
909
|
+
}
|
|
910
|
+
catch (error) {
|
|
911
|
+
assertErrorThrown(error);
|
|
912
|
+
addThinkingStep(setMessages, `Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
913
|
+
finishWithThinkingError(setMessages, error.message, startTime, classifyError(error));
|
|
914
|
+
}
|
|
435
915
|
return;
|
|
436
916
|
}
|
|
437
917
|
if (intent === LegendAIQuestionIntent.ORCHESTRATOR) {
|
|
438
918
|
if (config.orchestratorUrl && dataProductCoordinates) {
|
|
919
|
+
// When services are available, try SQL first even for ORCHESTRATOR intent
|
|
920
|
+
if (services.length > 0) {
|
|
921
|
+
const startTime = Date.now();
|
|
922
|
+
try {
|
|
923
|
+
addThinkingStep(setMessages, 'Preparing data query...');
|
|
924
|
+
await processDataQuery(question, services, coordinates, metadata, context, startTime, orchestratorOptions);
|
|
925
|
+
}
|
|
926
|
+
catch (error) {
|
|
927
|
+
assertErrorThrown(error);
|
|
928
|
+
addThinkingStep(setMessages, `Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
929
|
+
finishWithThinkingError(setMessages, error.message, startTime, classifyError(error));
|
|
930
|
+
}
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
439
933
|
await processQuestionViaOrchestrator(question, dataProductCoordinates, metadata, context, pureExecutionContext);
|
|
440
934
|
return;
|
|
441
935
|
}
|
|
@@ -448,7 +942,7 @@ export async function processQuestionWithIntent(question, intent, services, coor
|
|
|
448
942
|
catch (error) {
|
|
449
943
|
assertErrorThrown(error);
|
|
450
944
|
addThinkingStep(setMessages, `Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
451
|
-
finishWithThinkingError(setMessages, error.message, startTime);
|
|
945
|
+
finishWithThinkingError(setMessages, error.message, startTime, classifyError(error));
|
|
452
946
|
}
|
|
453
947
|
}
|
|
454
948
|
export const useLegendAIChatState = (services, coordinates, config, metadata, plugin, dataProductCoordinates, pureExecutionContext) => {
|
|
@@ -543,6 +1037,46 @@ export const useLegendAIChatState = (services, coordinates, config, metadata, pl
|
|
|
543
1037
|
dataProductCoordinates,
|
|
544
1038
|
pureExecutionContext,
|
|
545
1039
|
]);
|
|
1040
|
+
const runFallbackAction = useCallback((messageId) => {
|
|
1041
|
+
if (isSending || !config.orchestratorUrl || !dataProductCoordinates) {
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
// Find the user question associated with this assistant message
|
|
1045
|
+
let question;
|
|
1046
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1047
|
+
const msg = messages[i];
|
|
1048
|
+
if (msg?.role === LegendAIMessageRole.ASSISTANT &&
|
|
1049
|
+
msg.id === messageId &&
|
|
1050
|
+
i > 0) {
|
|
1051
|
+
const userMsg = messages[i - 1];
|
|
1052
|
+
if (userMsg?.role === LegendAIMessageRole.USER) {
|
|
1053
|
+
question = userMsg.text;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
if (!question) {
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
setIsSending(true);
|
|
1061
|
+
setMessages((prev) => prev.map((m) => m.id === messageId && m.role === LegendAIMessageRole.ASSISTANT
|
|
1062
|
+
? { ...m, fallbackAction: null, error: null, isProcessing: true }
|
|
1063
|
+
: m));
|
|
1064
|
+
const history = buildConversationHistory(messages);
|
|
1065
|
+
const q = question;
|
|
1066
|
+
processQuestionViaOrchestrator(q, dataProductCoordinates, metadata, { config, plugin, history, setMessages }, pureExecutionContext)
|
|
1067
|
+
.catch(noop())
|
|
1068
|
+
.finally(() => {
|
|
1069
|
+
setIsSending(false);
|
|
1070
|
+
});
|
|
1071
|
+
}, [
|
|
1072
|
+
isSending,
|
|
1073
|
+
messages,
|
|
1074
|
+
config,
|
|
1075
|
+
metadata,
|
|
1076
|
+
plugin,
|
|
1077
|
+
dataProductCoordinates,
|
|
1078
|
+
pureExecutionContext,
|
|
1079
|
+
]);
|
|
546
1080
|
return {
|
|
547
1081
|
questionText,
|
|
548
1082
|
setQuestionText,
|
|
@@ -550,6 +1084,7 @@ export const useLegendAIChatState = (services, coordinates, config, metadata, pl
|
|
|
550
1084
|
messages,
|
|
551
1085
|
askQuestion,
|
|
552
1086
|
askQuestionWithIntent,
|
|
1087
|
+
runFallbackAction,
|
|
553
1088
|
clearChat,
|
|
554
1089
|
expandedThinking,
|
|
555
1090
|
toggleThinking,
|