@finos/legend-lego 2.0.194 → 2.0.195
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 +0 -33
- package/lib/legend-ai/LegendAITypes.d.ts.map +1 -1
- package/lib/legend-ai/LegendAITypes.js +1 -39
- package/lib/legend-ai/LegendAITypes.js.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts +1 -96
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js +0 -56
- 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 +0 -6
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.js.map +1 -1
- package/lib/legend-ai/components/LegendAIChat.d.ts +1 -2
- package/lib/legend-ai/components/LegendAIChat.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIChat.js +10 -14
- package/lib/legend-ai/components/LegendAIChat.js.map +1 -1
- package/lib/legend-ai/index.d.ts +2 -5
- package/lib/legend-ai/index.d.ts.map +1 -1
- package/lib/legend-ai/index.js +2 -5
- package/lib/legend-ai/index.js.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatState.d.ts +5 -12
- package/lib/legend-ai/stores/LegendAIChatState.d.ts.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatState.js +69 -604
- package/lib/legend-ai/stores/LegendAIChatState.js.map +1 -1
- package/package.json +5 -5
- package/src/legend-ai/LegendAITypes.ts +1 -51
- package/src/legend-ai/LegendAI_LegendApplicationPlugin_Extension.ts +0 -169
- package/src/legend-ai/__test-utils__/LegendAITestUtils.ts +0 -9
- package/src/legend-ai/components/LegendAIChat.tsx +26 -74
- package/src/legend-ai/index.ts +0 -18
- package/src/legend-ai/stores/LegendAIChatState.ts +128 -1039
- package/tsconfig.json +0 -3
- package/lib/legend-ai/components/LegendAIAnalysisPanel.d.ts +0 -24
- package/lib/legend-ai/components/LegendAIAnalysisPanel.d.ts.map +0 -1
- package/lib/legend-ai/components/LegendAIAnalysisPanel.js +0 -35
- package/lib/legend-ai/components/LegendAIAnalysisPanel.js.map +0 -1
- package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts +0 -23
- package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts.map +0 -1
- package/lib/legend-ai/components/LegendAIAnalysisUtils.js +0 -168
- package/lib/legend-ai/components/LegendAIAnalysisUtils.js.map +0 -1
- package/lib/legend-ai/components/LegendAICharts.d.ts +0 -25
- package/lib/legend-ai/components/LegendAICharts.d.ts.map +0 -1
- package/lib/legend-ai/components/LegendAICharts.js +0 -70
- package/lib/legend-ai/components/LegendAICharts.js.map +0 -1
- package/src/legend-ai/components/LegendAIAnalysisPanel.tsx +0 -102
- package/src/legend-ai/components/LegendAIAnalysisUtils.ts +0 -226
- package/src/legend-ai/components/LegendAICharts.tsx +0 -166
|
@@ -15,30 +15,12 @@
|
|
|
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,
|
|
18
|
+
import { LegendAIQuestionIntent, LegendAIThinkingStepStatus, LegendAIMessageRole, TDSServiceSourceType, buildColumnDefsFromNames, } 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;
|
|
25
23
|
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
|
-
}
|
|
42
24
|
function deduplicateColumns(columns) {
|
|
43
25
|
const seen = new Map();
|
|
44
26
|
return columns.map((col) => {
|
|
@@ -47,7 +29,7 @@ function deduplicateColumns(columns) {
|
|
|
47
29
|
return count === 0 ? col : `${col}_${count + 1}`;
|
|
48
30
|
});
|
|
49
31
|
}
|
|
50
|
-
|
|
32
|
+
function createMessagePair(text) {
|
|
51
33
|
return [
|
|
52
34
|
{ id: uuid(), role: LegendAIMessageRole.USER, text },
|
|
53
35
|
{
|
|
@@ -56,17 +38,14 @@ export function createMessagePair(text) {
|
|
|
56
38
|
thinkingSteps: [],
|
|
57
39
|
sql: null,
|
|
58
40
|
textAnswer: null,
|
|
59
|
-
dataContext: null,
|
|
60
41
|
gridData: null,
|
|
61
42
|
error: null,
|
|
62
|
-
errorType: null,
|
|
63
43
|
sqlGenTime: null,
|
|
64
44
|
execTime: null,
|
|
65
45
|
thinkingDuration: null,
|
|
66
46
|
isProcessing: true,
|
|
67
47
|
isExecuting: false,
|
|
68
48
|
suggestedQueries: [],
|
|
69
|
-
fallbackAction: null,
|
|
70
49
|
},
|
|
71
50
|
];
|
|
72
51
|
}
|
|
@@ -87,7 +66,7 @@ export function addThinkingStep(setMessages, label) {
|
|
|
87
66
|
...msg.thinkingSteps.map((s) => s.status === LegendAIThinkingStepStatus.ACTIVE
|
|
88
67
|
? { ...s, status: LegendAIThinkingStepStatus.DONE }
|
|
89
68
|
: s),
|
|
90
|
-
{
|
|
69
|
+
{ label, status: LegendAIThinkingStepStatus.ACTIVE },
|
|
91
70
|
],
|
|
92
71
|
}));
|
|
93
72
|
}
|
|
@@ -98,21 +77,14 @@ export function completeThinkingSteps(setMessages) {
|
|
|
98
77
|
: s),
|
|
99
78
|
}));
|
|
100
79
|
}
|
|
101
|
-
export function
|
|
102
|
-
if (error instanceof LegendAIServiceError) {
|
|
103
|
-
return error.errorType;
|
|
104
|
-
}
|
|
105
|
-
return LegendAIErrorType.GENERAL;
|
|
106
|
-
}
|
|
107
|
-
export function finishWithThinkingError(setMessages, errorMsg, startTime, errorType) {
|
|
80
|
+
export function finishWithThinkingError(setMessages, errorMsg, startTime) {
|
|
108
81
|
updateLastAssistant(setMessages, (msg) => ({
|
|
109
82
|
thinkingSteps: msg.thinkingSteps.map((s) => s.status === LegendAIThinkingStepStatus.ACTIVE
|
|
110
83
|
? { ...s, status: LegendAIThinkingStepStatus.ERROR }
|
|
111
84
|
: s),
|
|
112
85
|
error: errorMsg.slice(0, MAX_ERROR_MESSAGE_LENGTH),
|
|
113
|
-
errorType: errorType ?? null,
|
|
114
86
|
isProcessing: false,
|
|
115
|
-
thinkingDuration:
|
|
87
|
+
thinkingDuration: ((Date.now() - startTime) / 1000).toFixed(1),
|
|
116
88
|
}));
|
|
117
89
|
}
|
|
118
90
|
export function buildConversationHistory(messages) {
|
|
@@ -224,7 +196,7 @@ export async function handleMetadataQuestion(question, metadata, context, startT
|
|
|
224
196
|
textAnswer: answer,
|
|
225
197
|
suggestedQueries,
|
|
226
198
|
isProcessing: false,
|
|
227
|
-
thinkingDuration:
|
|
199
|
+
thinkingDuration: ((Date.now() - startTime) / 1000).toFixed(1),
|
|
228
200
|
}));
|
|
229
201
|
}
|
|
230
202
|
export async function generateAndJudgeSql(question, services, coordinates, context, startTime) {
|
|
@@ -236,12 +208,12 @@ export async function generateAndJudgeSql(question, services, coordinates, conte
|
|
|
236
208
|
const { sql: generatedSql, failure, suggestion, } = plugin.extractSqlFromResponse(answerText);
|
|
237
209
|
if (failure) {
|
|
238
210
|
addThinkingStep(setMessages, `Generation failed: ${failure}`);
|
|
239
|
-
finishWithThinkingError(setMessages, buildGenerationFailureMessage(failure, suggestion, services), startTime
|
|
211
|
+
finishWithThinkingError(setMessages, buildGenerationFailureMessage(failure, suggestion, services), startTime);
|
|
240
212
|
return null;
|
|
241
213
|
}
|
|
242
214
|
if (!generatedSql) {
|
|
243
215
|
addThinkingStep(setMessages, 'Could not extract SQL from response');
|
|
244
|
-
finishWithThinkingError(setMessages, 'Could not extract SQL from LLM response.\nTry rephrasing your question or ask about a specific service.', startTime
|
|
216
|
+
finishWithThinkingError(setMessages, 'Could not extract SQL from LLM response.\nTry rephrasing your question or ask about a specific service.', startTime);
|
|
245
217
|
return null;
|
|
246
218
|
}
|
|
247
219
|
const maxAttempts = config.maxJudgeAttempts ?? DEFAULT_MAX_JUDGE_ATTEMPTS;
|
|
@@ -279,10 +251,10 @@ function reportExecutionResult(rawResult, setMessages, execStartTime, startTime)
|
|
|
279
251
|
completeThinkingSteps(setMessages);
|
|
280
252
|
updateLastAssistant(setMessages, () => ({
|
|
281
253
|
gridData: { columnDefs: buildColumnDefsFromNames(columns), rowData: rows },
|
|
282
|
-
execTime:
|
|
254
|
+
execTime: ((Date.now() - execStartTime) / 1000).toFixed(2),
|
|
283
255
|
isProcessing: false,
|
|
284
256
|
isExecuting: false,
|
|
285
|
-
thinkingDuration:
|
|
257
|
+
thinkingDuration: ((Date.now() - startTime) / 1000).toFixed(1),
|
|
286
258
|
}));
|
|
287
259
|
return { columns, rows };
|
|
288
260
|
}
|
|
@@ -297,13 +269,10 @@ export async function executeSqlAndReport(sql, services, config, plugin, setMess
|
|
|
297
269
|
}
|
|
298
270
|
catch (executeError) {
|
|
299
271
|
assertErrorThrown(executeError);
|
|
300
|
-
const execErrorType = classifyError(executeError);
|
|
301
272
|
addThinkingStep(setMessages, `Execution failed: ${executeError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
302
|
-
finishWithThinkingError(setMessages, buildExecutionErrorMessage(executeError.message, services), startTime
|
|
303
|
-
? LegendAIErrorType.EXECUTION
|
|
304
|
-
: execErrorType);
|
|
273
|
+
finishWithThinkingError(setMessages, buildExecutionErrorMessage(executeError.message, services), startTime);
|
|
305
274
|
updateLastAssistant(setMessages, () => ({
|
|
306
|
-
execTime:
|
|
275
|
+
execTime: ((Date.now() - execStartTime) / 1000).toFixed(2),
|
|
307
276
|
isExecuting: false,
|
|
308
277
|
}));
|
|
309
278
|
return undefined;
|
|
@@ -318,100 +287,25 @@ export async function executePureQueryAndReport(pureQuery, pureExecutionContext,
|
|
|
318
287
|
}
|
|
319
288
|
catch (executeError) {
|
|
320
289
|
assertErrorThrown(executeError);
|
|
321
|
-
const execErrorType = classifyError(executeError);
|
|
322
290
|
addThinkingStep(setMessages, `Execution failed: ${executeError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
323
291
|
completeThinkingSteps(setMessages);
|
|
324
292
|
updateLastAssistant(setMessages, () => ({
|
|
325
|
-
execTime:
|
|
293
|
+
execTime: ((Date.now() - execStartTime) / 1000).toFixed(2),
|
|
326
294
|
isExecuting: false,
|
|
327
295
|
isProcessing: false,
|
|
328
296
|
error: `Execution failed: ${executeError.message.slice(0, MAX_ERROR_MESSAGE_LENGTH)}`,
|
|
329
|
-
|
|
330
|
-
? LegendAIErrorType.EXECUTION
|
|
331
|
-
: execErrorType,
|
|
332
|
-
thinkingDuration: elapsedSeconds(startTime),
|
|
297
|
+
thinkingDuration: ((Date.now() - startTime) / 1000).toFixed(1),
|
|
333
298
|
}));
|
|
334
299
|
return { columns: [], rows: [] };
|
|
335
300
|
}
|
|
336
301
|
}
|
|
337
|
-
export async function
|
|
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) {
|
|
302
|
+
export async function processQuestionViaOrchestrator(question, dataProductCoordinates, _metadata, context, pureExecutionContext) {
|
|
402
303
|
const { config, plugin, setMessages } = context;
|
|
403
304
|
const startTime = Date.now();
|
|
404
305
|
try {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
}
|
|
306
|
+
addThinkingStep(setMessages, 'Resolving entities for your query...');
|
|
307
|
+
const resolvedEntities = await plugin.resolveEntitiesForQuery(question, dataProductCoordinates, config);
|
|
308
|
+
addThinkingStep(setMessages, `Found root entity: ${resolvedEntities.rootEntity.split('::').pop() ?? resolvedEntities.rootEntity}`);
|
|
415
309
|
if (resolvedEntities.relatedEntities.length > 0) {
|
|
416
310
|
addThinkingStep(setMessages, `Found ${resolvedEntities.relatedEntities.length} related entities`);
|
|
417
311
|
}
|
|
@@ -424,7 +318,7 @@ export async function processQuestionViaOrchestrator(question, dataProductCoordi
|
|
|
424
318
|
related_entities: resolvedEntities.relatedEntities,
|
|
425
319
|
},
|
|
426
320
|
}, config);
|
|
427
|
-
const queryGenTime =
|
|
321
|
+
const queryGenTime = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
428
322
|
completeThinkingSteps(setMessages);
|
|
429
323
|
updateLastAssistant(setMessages, () => ({
|
|
430
324
|
sql: orchestratorResponse.legend_query,
|
|
@@ -437,465 +331,98 @@ export async function processQuestionViaOrchestrator(question, dataProductCoordi
|
|
|
437
331
|
isProcessing: false,
|
|
438
332
|
isExecuting: false,
|
|
439
333
|
error: 'No execution context available — cannot execute query via engine.',
|
|
440
|
-
|
|
441
|
-
thinkingDuration: elapsedSeconds(startTime),
|
|
334
|
+
thinkingDuration: ((Date.now() - startTime) / 1000).toFixed(1),
|
|
442
335
|
}));
|
|
443
336
|
return;
|
|
444
337
|
}
|
|
445
|
-
|
|
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
|
-
}
|
|
338
|
+
await executePureQueryAndReport(orchestratorResponse.legend_query, pureExecutionContext, dataProductCoordinates, config, plugin, setMessages, startTime);
|
|
464
339
|
}
|
|
465
340
|
catch (error) {
|
|
466
341
|
assertErrorThrown(error);
|
|
467
|
-
const orchErrorType = classifyError(error);
|
|
468
342
|
addThinkingStep(setMessages, `Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
469
|
-
|
|
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);
|
|
343
|
+
finishWithThinkingError(setMessages, error.message, startTime);
|
|
486
344
|
}
|
|
487
345
|
}
|
|
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
|
-
}
|
|
793
346
|
async function processDataQuery(question, services, coordinates, metadata, context, startTime, orchestratorOptions) {
|
|
794
|
-
const { config, setMessages } = context;
|
|
347
|
+
const { config, plugin, setMessages } = context;
|
|
795
348
|
const dataProductCoordinates = orchestratorOptions?.dataProductCoordinates;
|
|
796
|
-
const
|
|
349
|
+
const pureExecutionContext = orchestratorOptions?.pureExecutionContext;
|
|
797
350
|
if (services.length === 0) {
|
|
798
|
-
|
|
351
|
+
if (config.orchestratorUrl && dataProductCoordinates) {
|
|
352
|
+
completeThinkingSteps(setMessages);
|
|
353
|
+
await processQuestionViaOrchestrator(question, dataProductCoordinates, metadata, context, pureExecutionContext);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
finishWithThinkingError(setMessages, 'No TDS services available for querying', startTime);
|
|
799
357
|
return;
|
|
800
358
|
}
|
|
801
359
|
addThinkingStep(setMessages, 'Found relevant services to query');
|
|
802
|
-
const
|
|
803
|
-
const judgedSql = await generateAndJudgeSql(question, selectedServices, coordinates, context, startTime);
|
|
360
|
+
const judgedSql = await generateAndJudgeSql(question, services, coordinates, context, startTime);
|
|
804
361
|
if (!judgedSql) {
|
|
805
|
-
|
|
806
|
-
|
|
362
|
+
if (config.orchestratorUrl && dataProductCoordinates) {
|
|
363
|
+
addThinkingStep(setMessages, 'SQL generation could not handle this query, trying Legend AI orchestrator...');
|
|
364
|
+
updateLastAssistant(setMessages, () => ({
|
|
365
|
+
error: null,
|
|
366
|
+
isProcessing: true,
|
|
367
|
+
}));
|
|
368
|
+
await processQuestionViaOrchestrator(question, dataProductCoordinates, metadata, context, pureExecutionContext);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
807
371
|
return;
|
|
808
372
|
}
|
|
809
|
-
const sqlGenTimeValue =
|
|
373
|
+
const sqlGenTimeValue = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
810
374
|
completeThinkingSteps(setMessages);
|
|
811
375
|
updateLastAssistant(setMessages, () => ({
|
|
812
376
|
sql: judgedSql,
|
|
813
377
|
sqlGenTime: sqlGenTimeValue,
|
|
814
378
|
isExecuting: true,
|
|
815
379
|
}));
|
|
816
|
-
const
|
|
817
|
-
if (
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
? LegendAIErrorType.EXECUTION
|
|
822
|
-
: execErrorType);
|
|
380
|
+
const sqlResult = await executeSqlAndReport(judgedSql, services, config, plugin, setMessages, startTime, dataProductCoordinates);
|
|
381
|
+
if (sqlResult?.rows.length === 0 &&
|
|
382
|
+
config.orchestratorUrl &&
|
|
383
|
+
dataProductCoordinates) {
|
|
384
|
+
addThinkingStep(setMessages, 'SQL query returned no results, trying Legend AI orchestrator...');
|
|
823
385
|
updateLastAssistant(setMessages, () => ({
|
|
386
|
+
gridData: null,
|
|
387
|
+
error: null,
|
|
388
|
+
isProcessing: true,
|
|
824
389
|
isExecuting: false,
|
|
825
|
-
...(hasOrchestratorFallback
|
|
826
|
-
? {
|
|
827
|
-
fallbackAction: {
|
|
828
|
-
label: 'Try Legend AI Orchestrator',
|
|
829
|
-
actionId: LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
|
|
830
|
-
},
|
|
831
|
-
}
|
|
832
|
-
: {}),
|
|
833
390
|
}));
|
|
834
|
-
|
|
391
|
+
await processQuestionViaOrchestrator(question, dataProductCoordinates, metadata, context, pureExecutionContext);
|
|
835
392
|
}
|
|
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);
|
|
852
393
|
}
|
|
853
394
|
export async function processQuestion(question, services, coordinates, metadata, context, dataProductCoordinates, pureExecutionContext) {
|
|
854
395
|
const { config, plugin, setMessages } = context;
|
|
855
396
|
const startTime = Date.now();
|
|
856
397
|
try {
|
|
857
398
|
addThinkingStep(setMessages, 'Analyzing your question...');
|
|
858
|
-
const
|
|
399
|
+
const serviceNames = services.map((s) => s.title);
|
|
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
|
|
859
414
|
? {
|
|
860
415
|
dataProductCoordinates,
|
|
861
416
|
...(pureExecutionContext === undefined
|
|
862
417
|
? {}
|
|
863
418
|
: { pureExecutionContext }),
|
|
864
419
|
}
|
|
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
|
-
}
|
|
420
|
+
: undefined);
|
|
894
421
|
}
|
|
895
422
|
catch (error) {
|
|
896
423
|
assertErrorThrown(error);
|
|
897
424
|
addThinkingStep(setMessages, `Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
898
|
-
finishWithThinkingError(setMessages, error.message, startTime
|
|
425
|
+
finishWithThinkingError(setMessages, error.message, startTime);
|
|
899
426
|
}
|
|
900
427
|
}
|
|
901
428
|
export async function processQuestionWithIntent(question, intent, services, coordinates, metadata, context, orchestratorOptions) {
|
|
@@ -904,32 +431,11 @@ export async function processQuestionWithIntent(question, intent, services, coor
|
|
|
904
431
|
const pureExecutionContext = orchestratorOptions?.pureExecutionContext;
|
|
905
432
|
if (intent === LegendAIQuestionIntent.METADATA) {
|
|
906
433
|
const startTime = Date.now();
|
|
907
|
-
|
|
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
|
-
}
|
|
434
|
+
await handleMetadataQuestion(question, metadata, context, startTime, services.length > 0);
|
|
915
435
|
return;
|
|
916
436
|
}
|
|
917
437
|
if (intent === LegendAIQuestionIntent.ORCHESTRATOR) {
|
|
918
438
|
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
|
-
}
|
|
933
439
|
await processQuestionViaOrchestrator(question, dataProductCoordinates, metadata, context, pureExecutionContext);
|
|
934
440
|
return;
|
|
935
441
|
}
|
|
@@ -942,7 +448,7 @@ export async function processQuestionWithIntent(question, intent, services, coor
|
|
|
942
448
|
catch (error) {
|
|
943
449
|
assertErrorThrown(error);
|
|
944
450
|
addThinkingStep(setMessages, `Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
945
|
-
finishWithThinkingError(setMessages, error.message, startTime
|
|
451
|
+
finishWithThinkingError(setMessages, error.message, startTime);
|
|
946
452
|
}
|
|
947
453
|
}
|
|
948
454
|
export const useLegendAIChatState = (services, coordinates, config, metadata, plugin, dataProductCoordinates, pureExecutionContext) => {
|
|
@@ -1037,46 +543,6 @@ export const useLegendAIChatState = (services, coordinates, config, metadata, pl
|
|
|
1037
543
|
dataProductCoordinates,
|
|
1038
544
|
pureExecutionContext,
|
|
1039
545
|
]);
|
|
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
|
-
]);
|
|
1080
546
|
return {
|
|
1081
547
|
questionText,
|
|
1082
548
|
setQuestionText,
|
|
@@ -1084,7 +550,6 @@ export const useLegendAIChatState = (services, coordinates, config, metadata, pl
|
|
|
1084
550
|
messages,
|
|
1085
551
|
askQuestion,
|
|
1086
552
|
askQuestionWithIntent,
|
|
1087
|
-
runFallbackAction,
|
|
1088
553
|
clearChat,
|
|
1089
554
|
expandedThinking,
|
|
1090
555
|
toggleThinking,
|