@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.
Files changed (48) hide show
  1. package/lib/index.css +2 -2
  2. package/lib/index.css.map +1 -1
  3. package/lib/legend-ai/LegendAITypes.d.ts +33 -0
  4. package/lib/legend-ai/LegendAITypes.d.ts.map +1 -1
  5. package/lib/legend-ai/LegendAITypes.js +39 -1
  6. package/lib/legend-ai/LegendAITypes.js.map +1 -1
  7. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts +96 -1
  8. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts.map +1 -1
  9. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js +56 -0
  10. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js.map +1 -1
  11. package/lib/legend-ai/__test-utils__/LegendAITestUtils.d.ts.map +1 -1
  12. package/lib/legend-ai/__test-utils__/LegendAITestUtils.js +6 -0
  13. package/lib/legend-ai/__test-utils__/LegendAITestUtils.js.map +1 -1
  14. package/lib/legend-ai/components/LegendAIAnalysisPanel.d.ts +24 -0
  15. package/lib/legend-ai/components/LegendAIAnalysisPanel.d.ts.map +1 -0
  16. package/lib/legend-ai/components/LegendAIAnalysisPanel.js +35 -0
  17. package/lib/legend-ai/components/LegendAIAnalysisPanel.js.map +1 -0
  18. package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts +23 -0
  19. package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts.map +1 -0
  20. package/lib/legend-ai/components/LegendAIAnalysisUtils.js +168 -0
  21. package/lib/legend-ai/components/LegendAIAnalysisUtils.js.map +1 -0
  22. package/lib/legend-ai/components/LegendAICharts.d.ts +25 -0
  23. package/lib/legend-ai/components/LegendAICharts.d.ts.map +1 -0
  24. package/lib/legend-ai/components/LegendAICharts.js +70 -0
  25. package/lib/legend-ai/components/LegendAICharts.js.map +1 -0
  26. package/lib/legend-ai/components/LegendAIChat.d.ts +2 -1
  27. package/lib/legend-ai/components/LegendAIChat.d.ts.map +1 -1
  28. package/lib/legend-ai/components/LegendAIChat.js +14 -10
  29. package/lib/legend-ai/components/LegendAIChat.js.map +1 -1
  30. package/lib/legend-ai/index.d.ts +5 -2
  31. package/lib/legend-ai/index.d.ts.map +1 -1
  32. package/lib/legend-ai/index.js +5 -2
  33. package/lib/legend-ai/index.js.map +1 -1
  34. package/lib/legend-ai/stores/LegendAIChatState.d.ts +12 -5
  35. package/lib/legend-ai/stores/LegendAIChatState.d.ts.map +1 -1
  36. package/lib/legend-ai/stores/LegendAIChatState.js +604 -69
  37. package/lib/legend-ai/stores/LegendAIChatState.js.map +1 -1
  38. package/package.json +5 -5
  39. package/src/legend-ai/LegendAITypes.ts +51 -1
  40. package/src/legend-ai/LegendAI_LegendApplicationPlugin_Extension.ts +169 -0
  41. package/src/legend-ai/__test-utils__/LegendAITestUtils.ts +9 -0
  42. package/src/legend-ai/components/LegendAIAnalysisPanel.tsx +102 -0
  43. package/src/legend-ai/components/LegendAIAnalysisUtils.ts +226 -0
  44. package/src/legend-ai/components/LegendAICharts.tsx +166 -0
  45. package/src/legend-ai/components/LegendAIChat.tsx +74 -26
  46. package/src/legend-ai/index.ts +18 -0
  47. package/src/legend-ai/stores/LegendAIChatState.ts +1039 -128
  48. 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 finishWithThinkingError(setMessages, errorMsg, startTime) {
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: ((Date.now() - startTime) / 1000).toFixed(1),
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: ((Date.now() - startTime) / 1000).toFixed(1),
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: ((Date.now() - execStartTime) / 1000).toFixed(2),
282
+ execTime: elapsedSeconds(execStartTime, 2),
255
283
  isProcessing: false,
256
284
  isExecuting: false,
257
- thinkingDuration: ((Date.now() - startTime) / 1000).toFixed(1),
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: ((Date.now() - execStartTime) / 1000).toFixed(2),
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: ((Date.now() - execStartTime) / 1000).toFixed(2),
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
- thinkingDuration: ((Date.now() - startTime) / 1000).toFixed(1),
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 processQuestionViaOrchestrator(question, dataProductCoordinates, _metadata, context, pureExecutionContext) {
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
- 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}`);
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 = ((Date.now() - startTime) / 1000).toFixed(2);
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
- thinkingDuration: ((Date.now() - startTime) / 1000).toFixed(1),
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
- finishWithThinkingError(setMessages, error.message, startTime);
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, plugin, setMessages } = context;
794
+ const { config, setMessages } = context;
348
795
  const dataProductCoordinates = orchestratorOptions?.dataProductCoordinates;
349
- const pureExecutionContext = orchestratorOptions?.pureExecutionContext;
796
+ const hasOrchestratorFallback = Boolean(config.orchestratorUrl && dataProductCoordinates);
350
797
  if (services.length === 0) {
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);
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 judgedSql = await generateAndJudgeSql(question, services, coordinates, context, startTime);
802
+ const selectedServices = await selectBestServices(question, services, context);
803
+ const judgedSql = await generateAndJudgeSql(question, selectedServices, coordinates, context, startTime);
361
804
  if (!judgedSql) {
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
- }
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 = ((Date.now() - startTime) / 1000).toFixed(2);
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 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...');
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
- await processQuestionViaOrchestrator(question, dataProductCoordinates, metadata, context, pureExecutionContext);
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 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
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
- await handleMetadataQuestion(question, metadata, context, startTime, services.length > 0);
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,