@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.
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 +0 -33
  4. package/lib/legend-ai/LegendAITypes.d.ts.map +1 -1
  5. package/lib/legend-ai/LegendAITypes.js +1 -39
  6. package/lib/legend-ai/LegendAITypes.js.map +1 -1
  7. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts +1 -96
  8. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts.map +1 -1
  9. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js +0 -56
  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 +0 -6
  13. package/lib/legend-ai/__test-utils__/LegendAITestUtils.js.map +1 -1
  14. package/lib/legend-ai/components/LegendAIChat.d.ts +1 -2
  15. package/lib/legend-ai/components/LegendAIChat.d.ts.map +1 -1
  16. package/lib/legend-ai/components/LegendAIChat.js +10 -14
  17. package/lib/legend-ai/components/LegendAIChat.js.map +1 -1
  18. package/lib/legend-ai/index.d.ts +2 -5
  19. package/lib/legend-ai/index.d.ts.map +1 -1
  20. package/lib/legend-ai/index.js +2 -5
  21. package/lib/legend-ai/index.js.map +1 -1
  22. package/lib/legend-ai/stores/LegendAIChatState.d.ts +5 -12
  23. package/lib/legend-ai/stores/LegendAIChatState.d.ts.map +1 -1
  24. package/lib/legend-ai/stores/LegendAIChatState.js +69 -604
  25. package/lib/legend-ai/stores/LegendAIChatState.js.map +1 -1
  26. package/package.json +5 -5
  27. package/src/legend-ai/LegendAITypes.ts +1 -51
  28. package/src/legend-ai/LegendAI_LegendApplicationPlugin_Extension.ts +0 -169
  29. package/src/legend-ai/__test-utils__/LegendAITestUtils.ts +0 -9
  30. package/src/legend-ai/components/LegendAIChat.tsx +26 -74
  31. package/src/legend-ai/index.ts +0 -18
  32. package/src/legend-ai/stores/LegendAIChatState.ts +128 -1039
  33. package/tsconfig.json +0 -3
  34. package/lib/legend-ai/components/LegendAIAnalysisPanel.d.ts +0 -24
  35. package/lib/legend-ai/components/LegendAIAnalysisPanel.d.ts.map +0 -1
  36. package/lib/legend-ai/components/LegendAIAnalysisPanel.js +0 -35
  37. package/lib/legend-ai/components/LegendAIAnalysisPanel.js.map +0 -1
  38. package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts +0 -23
  39. package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts.map +0 -1
  40. package/lib/legend-ai/components/LegendAIAnalysisUtils.js +0 -168
  41. package/lib/legend-ai/components/LegendAIAnalysisUtils.js.map +0 -1
  42. package/lib/legend-ai/components/LegendAICharts.d.ts +0 -25
  43. package/lib/legend-ai/components/LegendAICharts.d.ts.map +0 -1
  44. package/lib/legend-ai/components/LegendAICharts.js +0 -70
  45. package/lib/legend-ai/components/LegendAICharts.js.map +0 -1
  46. package/src/legend-ai/components/LegendAIAnalysisPanel.tsx +0 -102
  47. package/src/legend-ai/components/LegendAIAnalysisUtils.ts +0 -226
  48. 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, LegendAIErrorType, LegendAIServiceError, TDSServiceSourceType, buildColumnDefsFromNames, LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID, } from '../LegendAITypes.js';
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
- export function createMessagePair(text) {
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
- { id: uuid(), label, status: LegendAIThinkingStepStatus.ACTIVE },
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 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) {
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: elapsedSeconds(startTime),
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: elapsedSeconds(startTime),
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, LegendAIErrorType.GENERATION);
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, LegendAIErrorType.GENERATION);
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: elapsedSeconds(execStartTime, 2),
254
+ execTime: ((Date.now() - execStartTime) / 1000).toFixed(2),
283
255
  isProcessing: false,
284
256
  isExecuting: false,
285
- thinkingDuration: elapsedSeconds(startTime),
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, execErrorType === LegendAIErrorType.GENERAL
303
- ? LegendAIErrorType.EXECUTION
304
- : execErrorType);
273
+ finishWithThinkingError(setMessages, buildExecutionErrorMessage(executeError.message, services), startTime);
305
274
  updateLastAssistant(setMessages, () => ({
306
- execTime: elapsedSeconds(execStartTime, 2),
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: elapsedSeconds(execStartTime, 2),
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
- errorType: execErrorType === LegendAIErrorType.GENERAL
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 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) {
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
- 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
- }
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 = elapsedSeconds(startTime, 2);
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
- errorType: LegendAIErrorType.EXECUTION,
441
- thinkingDuration: elapsedSeconds(startTime),
334
+ thinkingDuration: ((Date.now() - startTime) / 1000).toFixed(1),
442
335
  }));
443
336
  return;
444
337
  }
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
- }
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
- 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);
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 hasOrchestratorFallback = Boolean(config.orchestratorUrl && dataProductCoordinates);
349
+ const pureExecutionContext = orchestratorOptions?.pureExecutionContext;
797
350
  if (services.length === 0) {
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);
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 selectedServices = await selectBestServices(question, services, context);
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
- 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);
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 = elapsedSeconds(startTime, 2);
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 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);
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
- return;
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 orchestratorOpts = dataProductCoordinates
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, classifyError(error));
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
- 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
- }
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, classifyError(error));
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,