@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
@@ -25,21 +25,16 @@ import {
25
25
  type LegendAIMessage,
26
26
  type LegendAIConversationTurn,
27
27
  type LegendAIProductMetadata,
28
- type LegendAIFallbackAction,
29
28
  LegendAIQuestionIntent,
30
29
  LegendAIThinkingStepStatus,
31
30
  LegendAIMessageRole,
32
- LegendAIErrorType,
33
- LegendAIServiceError,
34
31
  TDSServiceSourceType,
35
32
  buildColumnDefsFromNames,
36
- LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
37
33
  } from '../LegendAITypes.js';
38
34
  import {
39
35
  type LegendAI_LegendApplicationPlugin_Extension,
40
36
  type LegendAIOrchestratorDataProductCoordinates,
41
37
  type LegendAISqlExecutionResultData,
42
- type LegendAIResolvedEntities,
43
38
  LegendAIJudgeVerdict,
44
39
  } from '../LegendAI_LegendApplicationPlugin_Extension.js';
45
40
  import type { QueryExplicitExecutionContextInfo } from '@finos/legend-graph';
@@ -47,32 +42,9 @@ import type { QueryExplicitExecutionContextInfo } from '@finos/legend-graph';
47
42
  const MAX_ERROR_MESSAGE_LENGTH = 500;
48
43
  const MAX_THINKING_ERROR_PREVIEW_LENGTH = 200;
49
44
  const DEFAULT_MAX_JUDGE_ATTEMPTS = 5;
50
- const DEFAULT_MAX_EXECUTION_RETRIES = 3;
51
- const ANALYSIS_TIMEOUT_MS = 15_000;
52
45
 
53
46
  const SUGGESTED_QUERIES_DELIMITER = '---SUGGESTED_QUERIES---';
54
47
 
55
- export function elapsedSeconds(startTime: number, decimals: 1 | 2 = 1): string {
56
- return ((Date.now() - startTime) / 1000).toFixed(decimals);
57
- }
58
-
59
- function withTimeout<T>(
60
- promise: Promise<T>,
61
- ms: number,
62
- ): Promise<T | undefined> {
63
- let timer: ReturnType<typeof setTimeout> | undefined;
64
- return Promise.race([
65
- promise.finally(() => {
66
- if (timer !== undefined) {
67
- clearTimeout(timer);
68
- }
69
- }),
70
- new Promise<undefined>((resolve) => {
71
- timer = setTimeout(() => resolve(undefined), ms);
72
- }),
73
- ]);
74
- }
75
-
76
48
  function deduplicateColumns(columns: string[]): string[] {
77
49
  const seen = new Map<string, number>();
78
50
  return columns.map((col) => {
@@ -86,7 +58,7 @@ export type MessageSetter = React.Dispatch<
86
58
  React.SetStateAction<LegendAIMessage[]>
87
59
  >;
88
60
 
89
- export function createMessagePair(
61
+ function createMessagePair(
90
62
  text: string,
91
63
  ): [LegendAIUserMessage, LegendAIAssistantMessage] {
92
64
  return [
@@ -97,22 +69,19 @@ export function createMessagePair(
97
69
  thinkingSteps: [],
98
70
  sql: null,
99
71
  textAnswer: null,
100
- dataContext: null,
101
72
  gridData: null,
102
73
  error: null,
103
- errorType: null,
104
74
  sqlGenTime: null,
105
75
  execTime: null,
106
76
  thinkingDuration: null,
107
77
  isProcessing: true,
108
78
  isExecuting: false,
109
79
  suggestedQueries: [],
110
- fallbackAction: null,
111
80
  },
112
81
  ];
113
82
  }
114
83
 
115
- export interface LegendAIOperationContext {
84
+ interface LegendAIOperationContext {
116
85
  config: LegendAIConfig;
117
86
  plugin: LegendAI_LegendApplicationPlugin_Extension;
118
87
  history: LegendAIConversationTurn[];
@@ -150,7 +119,7 @@ export function addThinkingStep(
150
119
  ? { ...s, status: LegendAIThinkingStepStatus.DONE }
151
120
  : s,
152
121
  ),
153
- { id: uuid(), label, status: LegendAIThinkingStepStatus.ACTIVE },
122
+ { label, status: LegendAIThinkingStepStatus.ACTIVE },
154
123
  ],
155
124
  }));
156
125
  }
@@ -165,18 +134,10 @@ export function completeThinkingSteps(setMessages: MessageSetter): void {
165
134
  }));
166
135
  }
167
136
 
168
- export function classifyError(error: Error): LegendAIErrorType {
169
- if (error instanceof LegendAIServiceError) {
170
- return error.errorType;
171
- }
172
- return LegendAIErrorType.GENERAL;
173
- }
174
-
175
137
  export function finishWithThinkingError(
176
138
  setMessages: MessageSetter,
177
139
  errorMsg: string,
178
140
  startTime: number,
179
- errorType?: LegendAIErrorType,
180
141
  ): void {
181
142
  updateLastAssistant(setMessages, (msg) => ({
182
143
  thinkingSteps: msg.thinkingSteps.map((s) =>
@@ -185,9 +146,8 @@ export function finishWithThinkingError(
185
146
  : s,
186
147
  ),
187
148
  error: errorMsg.slice(0, MAX_ERROR_MESSAGE_LENGTH),
188
- errorType: errorType ?? null,
189
149
  isProcessing: false,
190
- thinkingDuration: elapsedSeconds(startTime),
150
+ thinkingDuration: ((Date.now() - startTime) / 1000).toFixed(1),
191
151
  }));
192
152
  }
193
153
 
@@ -346,7 +306,7 @@ export async function handleMetadataQuestion(
346
306
  textAnswer: answer,
347
307
  suggestedQueries,
348
308
  isProcessing: false,
349
- thinkingDuration: elapsedSeconds(startTime),
309
+ thinkingDuration: ((Date.now() - startTime) / 1000).toFixed(1),
350
310
  }));
351
311
  }
352
312
 
@@ -380,7 +340,6 @@ export async function generateAndJudgeSql(
380
340
  setMessages,
381
341
  buildGenerationFailureMessage(failure, suggestion, services),
382
342
  startTime,
383
- LegendAIErrorType.GENERATION,
384
343
  );
385
344
  return null;
386
345
  }
@@ -391,7 +350,6 @@ export async function generateAndJudgeSql(
391
350
  setMessages,
392
351
  'Could not extract SQL from LLM response.\nTry rephrasing your question or ask about a specific service.',
393
352
  startTime,
394
- LegendAIErrorType.GENERATION,
395
353
  );
396
354
  return null;
397
355
  }
@@ -461,10 +419,10 @@ function reportExecutionResult(
461
419
 
462
420
  updateLastAssistant(setMessages, () => ({
463
421
  gridData: { columnDefs: buildColumnDefsFromNames(columns), rowData: rows },
464
- execTime: elapsedSeconds(execStartTime, 2),
422
+ execTime: ((Date.now() - execStartTime) / 1000).toFixed(2),
465
423
  isProcessing: false,
466
424
  isExecuting: false,
467
- thinkingDuration: elapsedSeconds(startTime),
425
+ thinkingDuration: ((Date.now() - startTime) / 1000).toFixed(1),
468
426
  }));
469
427
  return { columns, rows };
470
428
  }
@@ -495,7 +453,6 @@ export async function executeSqlAndReport(
495
453
  );
496
454
  } catch (executeError) {
497
455
  assertErrorThrown(executeError);
498
- const execErrorType = classifyError(executeError);
499
456
  addThinkingStep(
500
457
  setMessages,
501
458
  `Execution failed: ${executeError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
@@ -504,12 +461,9 @@ export async function executeSqlAndReport(
504
461
  setMessages,
505
462
  buildExecutionErrorMessage(executeError.message, services),
506
463
  startTime,
507
- execErrorType === LegendAIErrorType.GENERAL
508
- ? LegendAIErrorType.EXECUTION
509
- : execErrorType,
510
464
  );
511
465
  updateLastAssistant(setMessages, () => ({
512
- execTime: elapsedSeconds(execStartTime, 2),
466
+ execTime: ((Date.now() - execStartTime) / 1000).toFixed(2),
513
467
  isExecuting: false,
514
468
  }));
515
469
  return undefined;
@@ -542,183 +496,44 @@ export async function executePureQueryAndReport(
542
496
  );
543
497
  } catch (executeError) {
544
498
  assertErrorThrown(executeError);
545
- const execErrorType = classifyError(executeError);
546
499
  addThinkingStep(
547
500
  setMessages,
548
501
  `Execution failed: ${executeError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
549
502
  );
550
503
  completeThinkingSteps(setMessages);
551
504
  updateLastAssistant(setMessages, () => ({
552
- execTime: elapsedSeconds(execStartTime, 2),
505
+ execTime: ((Date.now() - execStartTime) / 1000).toFixed(2),
553
506
  isExecuting: false,
554
507
  isProcessing: false,
555
508
  error: `Execution failed: ${executeError.message.slice(0, MAX_ERROR_MESSAGE_LENGTH)}`,
556
- errorType:
557
- execErrorType === LegendAIErrorType.GENERAL
558
- ? LegendAIErrorType.EXECUTION
559
- : execErrorType,
560
- thinkingDuration: elapsedSeconds(startTime),
509
+ thinkingDuration: ((Date.now() - startTime) / 1000).toFixed(1),
561
510
  }));
562
511
  return { columns: [], rows: [] };
563
512
  }
564
513
  }
565
514
 
566
- export async function analyzeOrchestratorResults(
567
- question: string,
568
- query: string,
569
- execResult: LegendAISqlExecutionResultData,
570
- metadata: LegendAIProductMetadata,
571
- context: LegendAIOperationContext,
572
- startTime: number,
573
- ): Promise<void> {
574
- const { config, plugin, setMessages } = context;
575
- addThinkingStep(setMessages, 'Analyzing results...');
576
- updateLastAssistant(setMessages, () => ({
577
- isProcessing: true,
578
- }));
579
- const analysis = await withTimeout(
580
- plugin.analyzeQueryResults(
581
- question,
582
- query,
583
- execResult.columns,
584
- execResult.rows,
585
- metadata,
586
- config,
587
- ),
588
- ANALYSIS_TIMEOUT_MS,
589
- );
590
- if (analysis) {
591
- completeThinkingSteps(setMessages);
592
- updateLastAssistant(setMessages, () => ({
593
- textAnswer: analysis.summary,
594
- suggestedQueries: analysis.suggestedQueries,
595
- isProcessing: false,
596
- thinkingDuration: elapsedSeconds(startTime),
597
- }));
598
- }
599
- }
600
-
601
- async function handleEmptyOrchestratorResults(
602
- question: string,
603
- legendQuery: string,
604
- orchestratorOptions: Required<LegendAIOrchestratorOptionsParam>,
605
- metadata: LegendAIProductMetadata,
606
- resolvedEntities: LegendAIResolvedEntities,
607
- context: LegendAIOperationContext,
608
- startTime: number,
609
- ): Promise<void> {
610
- const { dataProductCoordinates, pureExecutionContext } = orchestratorOptions;
611
- const { config, plugin, setMessages } = context;
612
-
613
- if (resolvedEntities.relatedEntities.length > 0) {
614
- const alternateRoot = resolvedEntities.relatedEntities[0];
615
- if (alternateRoot) {
616
- addThinkingStep(
617
- setMessages,
618
- `No results with ${resolvedEntities.rootEntity.split('::').pop() ?? resolvedEntities.rootEntity}, retrying with ${alternateRoot.split('::').pop() ?? alternateRoot}...`,
619
- );
620
-
621
- try {
622
- const retryResponse = await plugin.generateQueryViaOrchestrator(
623
- {
624
- user_question: question,
625
- semantic_search_resolution_details: {
626
- data_product_coordinates: dataProductCoordinates,
627
- root_entity: alternateRoot,
628
- related_entities: resolvedEntities.relatedEntities.slice(1),
629
- },
630
- },
631
- config,
632
- );
633
-
634
- updateLastAssistant(setMessages, () => ({
635
- sql: retryResponse.legend_query,
636
- sqlGenTime: elapsedSeconds(startTime, 2),
637
- isExecuting: true,
638
- }));
639
-
640
- const retryResult = await executePureQueryAndReport(
641
- retryResponse.legend_query,
642
- pureExecutionContext,
643
- dataProductCoordinates,
644
- config,
645
- plugin,
646
- setMessages,
647
- startTime,
648
- );
649
-
650
- if (retryResult.rows.length > 0) {
651
- await analyzeOrchestratorResults(
652
- question,
653
- retryResponse.legend_query,
654
- retryResult,
655
- metadata,
656
- context,
657
- startTime,
658
- );
659
- return;
660
- }
661
- } catch {
662
- /* empty */
663
- }
664
- }
665
- }
666
-
667
- addThinkingStep(
668
- setMessages,
669
- 'No results returned \u2014 building contextual guidance...',
670
- );
671
- updateLastAssistant(setMessages, () => ({
672
- isProcessing: true,
673
- }));
674
- const fallback = await withTimeout(
675
- plugin.buildNoResultsFallback(question, legendQuery, metadata, config),
676
- ANALYSIS_TIMEOUT_MS,
677
- );
678
- if (fallback) {
679
- completeThinkingSteps(setMessages);
680
- updateLastAssistant(setMessages, () => ({
681
- textAnswer: fallback.summary,
682
- suggestedQueries: fallback.suggestedQueries,
683
- isProcessing: false,
684
- thinkingDuration: elapsedSeconds(startTime),
685
- }));
686
- }
687
- }
688
-
689
515
  export async function processQuestionViaOrchestrator(
690
516
  question: string,
691
517
  dataProductCoordinates: LegendAIOrchestratorDataProductCoordinates,
692
- metadata: LegendAIProductMetadata,
518
+ _metadata: LegendAIProductMetadata,
693
519
  context: LegendAIOperationContext,
694
520
  pureExecutionContext?: QueryExplicitExecutionContextInfo,
695
- preResolvedEntities?: LegendAIResolvedEntities,
696
521
  ): Promise<void> {
697
522
  const { config, plugin, setMessages } = context;
698
523
  const startTime = Date.now();
699
524
 
700
525
  try {
701
- let resolvedEntities: LegendAIResolvedEntities;
702
- if (preResolvedEntities) {
703
- resolvedEntities = preResolvedEntities;
704
- addThinkingStep(
705
- setMessages,
706
- `Using pre-resolved root entity: ${resolvedEntities.rootEntity.split('::').pop() ?? resolvedEntities.rootEntity}`,
707
- );
708
- } else {
709
- addThinkingStep(setMessages, 'Resolving entities for your query...');
710
- resolvedEntities = await plugin.resolveEntitiesForQuery(
711
- question,
712
- dataProductCoordinates,
713
- config,
714
- pureExecutionContext,
715
- );
716
- addThinkingStep(
717
- setMessages,
718
- `Found root entity: ${resolvedEntities.rootEntity.split('::').pop() ?? resolvedEntities.rootEntity}`,
719
- );
720
- }
526
+ addThinkingStep(setMessages, 'Resolving entities for your query...');
527
+ const resolvedEntities = await plugin.resolveEntitiesForQuery(
528
+ question,
529
+ dataProductCoordinates,
530
+ config,
531
+ );
721
532
 
533
+ addThinkingStep(
534
+ setMessages,
535
+ `Found root entity: ${resolvedEntities.rootEntity.split('::').pop() ?? resolvedEntities.rootEntity}`,
536
+ );
722
537
  if (resolvedEntities.relatedEntities.length > 0) {
723
538
  addThinkingStep(
724
539
  setMessages,
@@ -739,7 +554,7 @@ export async function processQuestionViaOrchestrator(
739
554
  config,
740
555
  );
741
556
 
742
- const queryGenTime = elapsedSeconds(startTime, 2);
557
+ const queryGenTime = ((Date.now() - startTime) / 1000).toFixed(2);
743
558
  completeThinkingSteps(setMessages);
744
559
  updateLastAssistant(setMessages, () => ({
745
560
  sql: orchestratorResponse.legend_query,
@@ -754,13 +569,12 @@ export async function processQuestionViaOrchestrator(
754
569
  isExecuting: false,
755
570
  error:
756
571
  'No execution context available — cannot execute query via engine.',
757
- errorType: LegendAIErrorType.EXECUTION,
758
- thinkingDuration: elapsedSeconds(startTime),
572
+ thinkingDuration: ((Date.now() - startTime) / 1000).toFixed(1),
759
573
  }));
760
574
  return;
761
575
  }
762
576
 
763
- const execResult = await executePureQueryAndReport(
577
+ await executePureQueryAndReport(
764
578
  orchestratorResponse.legend_query,
765
579
  pureExecutionContext,
766
580
  dataProductCoordinates,
@@ -769,633 +583,82 @@ export async function processQuestionViaOrchestrator(
769
583
  setMessages,
770
584
  startTime,
771
585
  );
772
-
773
- try {
774
- if (execResult.rows.length > 0) {
775
- await analyzeOrchestratorResults(
776
- question,
777
- orchestratorResponse.legend_query,
778
- execResult,
779
- metadata,
780
- context,
781
- startTime,
782
- );
783
- } else {
784
- await handleEmptyOrchestratorResults(
785
- question,
786
- orchestratorResponse.legend_query,
787
- { dataProductCoordinates, pureExecutionContext },
788
- metadata,
789
- resolvedEntities,
790
- context,
791
- startTime,
792
- );
793
- }
794
- } catch {
795
- /* empty */
796
- } finally {
797
- completeThinkingSteps(setMessages);
798
- updateLastAssistant(setMessages, () => ({
799
- isProcessing: false,
800
- thinkingDuration: elapsedSeconds(startTime),
801
- }));
802
- }
803
586
  } catch (error) {
804
587
  assertErrorThrown(error);
805
- const orchErrorType = classifyError(error);
806
588
  addThinkingStep(
807
589
  setMessages,
808
590
  `Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
809
591
  );
810
-
811
- try {
812
- addThinkingStep(
813
- setMessages,
814
- 'Building guidance from available metadata...',
815
- );
816
- const fallbackText = await withTimeout(
817
- plugin.buildFailureFallback(question, error.message, metadata, config),
818
- ANALYSIS_TIMEOUT_MS,
819
- );
820
- if (fallbackText) {
821
- completeThinkingSteps(setMessages);
822
- updateLastAssistant(setMessages, () => ({
823
- dataContext: fallbackText,
824
- isProcessing: false,
825
- thinkingDuration: elapsedSeconds(startTime),
826
- }));
827
- return;
828
- }
829
- } catch {
830
- /* empty */
831
- }
832
-
833
- finishWithThinkingError(
834
- setMessages,
835
- error.message,
836
- startTime,
837
- orchErrorType,
838
- );
839
- }
840
- }
841
-
842
- function cleanLlmSqlResponse(raw: string): string {
843
- return raw
844
- .trim()
845
- .replace(/^```\w*\n?/, '')
846
- .replace(/\n?```$/, '')
847
- .replace(/;\s*$/, '')
848
- .trim();
849
- }
850
-
851
- function isValidSqlCorrection(trimmed: string, currentSql: string): boolean {
852
- return (
853
- trimmed.length > 0 &&
854
- trimmed.toLowerCase().startsWith('select') &&
855
- trimmed !== currentSql
856
- );
857
- }
858
-
859
- const ALIAS_DOT_COL_PATTERN = /\b(?<tbl>[a-z]\w*)\s*\.\s*"(?<col>[^"]+)"/gi;
860
-
861
- const JOIN_PATTERN = /\bJOIN\b/i;
862
-
863
- const ORDER_BY_SPLIT = /\bORDER\s+BY\b/i;
864
-
865
- export function sanitizeJoinOrderBy(sql: string): string {
866
- if (!JOIN_PATTERN.test(sql)) {
867
- return sql;
868
- }
869
- const parts = sql.split(ORDER_BY_SPLIT);
870
- if (parts.length < 2) {
871
- return sql;
872
- }
873
-
874
- const beforeOrderBy = parts[0] ?? '';
875
- const afterOrderBy = parts.slice(1).join('ORDER BY').replace(/^\s+/, '');
876
-
877
- const selectAliases = new Map<string, string>();
878
- const aliasRegex =
879
- /\b(?<tbl>[a-z]\w*)\s*\.\s*"(?<col>[^"]+)"\s+AS\s+(?:"(?<qAlias>[^"]+)"|(?<uAlias>\w+))/gi;
880
- let m: RegExpExecArray | null;
881
- while ((m = aliasRegex.exec(beforeOrderBy)) !== null) {
882
- const tableAlias = (m.groups?.tbl ?? '').toLowerCase();
883
- const colName = (m.groups?.col ?? '').toLowerCase();
884
- const asAlias = m.groups?.qAlias ?? m.groups?.uAlias ?? '';
885
- selectAliases.set(`${tableAlias}.${colName}`, asAlias);
886
- }
887
-
888
- if (selectAliases.size === 0) {
889
- return sql;
890
- }
891
-
892
- const rewritten = afterOrderBy.replaceAll(
893
- ALIAS_DOT_COL_PATTERN,
894
- (...args) => {
895
- const groups = args[args.length - 1] as {
896
- tbl: string;
897
- col: string;
898
- };
899
- const key = `${groups.tbl.toLowerCase()}.${groups.col.toLowerCase()}`;
900
- const alias = selectAliases.get(key);
901
- return alias ? `"${alias}"` : String(args[0]);
902
- },
903
- );
904
-
905
- if (rewritten === afterOrderBy) {
906
- return sql;
907
- }
908
- return `${beforeOrderBy}ORDER BY ${rewritten}`;
909
- }
910
-
911
- const UNION_ALL_PATTERN = /\bUNION\s+ALL\b/i;
912
-
913
- const LITERAL_COL_PATTERN = /,\s*'[^']*'\s+AS\s+(?:"[^"]+"|[a-z]\w*)/gi;
914
-
915
- export function sanitizeLiteralColumns(sql: string): string {
916
- if (!UNION_ALL_PATTERN.test(sql)) {
917
- return sql;
918
- }
919
- LITERAL_COL_PATTERN.lastIndex = 0;
920
- if (!LITERAL_COL_PATTERN.test(sql)) {
921
- return sql;
922
- }
923
- LITERAL_COL_PATTERN.lastIndex = 0;
924
- return sql.replace(LITERAL_COL_PATTERN, '');
925
- }
926
-
927
- const SERVICE_PARAM_DATE_LIKE =
928
- /date|time|day|month|year|period|asOf|businessDate|processingDate|snapshot/i;
929
-
930
- function hasUnresolvableParams(service: TDSServiceSchema): boolean {
931
- return service.parameters.some((p) => !SERVICE_PARAM_DATE_LIKE.test(p));
932
- }
933
-
934
- function getNonDateParamNames(service: TDSServiceSchema): string[] {
935
- return service.parameters.filter((p) => !SERVICE_PARAM_DATE_LIKE.test(p));
936
- }
937
-
938
- export function stripNonDateServiceParams(sql: string): string {
939
- return sql.replaceAll(/,\s*\w+\s*=>\s*'[^']*'/g, (match) => {
940
- const paramName = /,\s*(?<param>\w+)\s*=>/.exec(match)?.groups?.param;
941
- if (!paramName) {
942
- return match;
943
- }
944
- if (
945
- paramName === 'coordinates' ||
946
- SERVICE_PARAM_DATE_LIKE.test(paramName)
947
- ) {
948
- return match;
949
- }
950
- return '';
951
- });
952
- }
953
-
954
- async function executeSqlForServices(
955
- sql: string,
956
- services: TDSServiceSchema[],
957
- dataProductCoordinates:
958
- | LegendAIOrchestratorDataProductCoordinates
959
- | undefined,
960
- plugin: LegendAI_LegendApplicationPlugin_Extension,
961
- config: LegendAIConfig,
962
- ): Promise<LegendAISqlExecutionResultData> {
963
- const safeSql = sanitizeLiteralColumns(sanitizeJoinOrderBy(sql));
964
- const isAccessPoint = services.some(
965
- (s) => s.sourceType === TDSServiceSourceType.ACCESS_POINT,
966
- );
967
- if (isAccessPoint && dataProductCoordinates) {
968
- return plugin.executeLakehouseSql(safeSql, dataProductCoordinates, config);
969
- }
970
- return plugin.executeSql(safeSql, config);
971
- }
972
-
973
- interface SqlExecutionOutcome {
974
- sql: string;
975
- result?: LegendAISqlExecutionResultData;
976
- error?: string;
977
- }
978
-
979
- async function executeSqlWithRetries(
980
- initialSql: string,
981
- question: string,
982
- services: TDSServiceSchema[],
983
- coordinates: string,
984
- dataProductCoordinates:
985
- | LegendAIOrchestratorDataProductCoordinates
986
- | undefined,
987
- context: LegendAIOperationContext,
988
- ): Promise<SqlExecutionOutcome> {
989
- const { plugin, config, setMessages } = context;
990
- let currentSql = initialSql;
991
-
992
- for (let attempt = 0; attempt <= DEFAULT_MAX_EXECUTION_RETRIES; attempt++) {
993
- try {
994
- const result = await executeSqlForServices(
995
- currentSql,
996
- services,
997
- dataProductCoordinates,
998
- plugin,
999
- config,
1000
- );
1001
- return { sql: currentSql, result };
1002
- } catch (executeError) {
1003
- assertErrorThrown(executeError);
1004
- if (attempt >= DEFAULT_MAX_EXECUTION_RETRIES) {
1005
- return { sql: currentSql, error: executeError.message };
1006
- }
1007
- addThinkingStep(
1008
- setMessages,
1009
- `Execution failed (attempt ${attempt + 1}/${DEFAULT_MAX_EXECUTION_RETRIES + 1}), correcting query...`,
1010
- );
1011
- const corrected = await attemptErrorCorrection(
1012
- currentSql,
1013
- executeError.message,
1014
- question,
1015
- services,
1016
- coordinates,
1017
- plugin,
1018
- config,
1019
- );
1020
- if (corrected) {
1021
- currentSql = corrected;
1022
- updateLastAssistant(setMessages, () => ({ sql: currentSql }));
1023
- continue;
1024
- }
1025
- return { sql: currentSql, error: executeError.message };
1026
- }
1027
- }
1028
- return { sql: currentSql };
1029
- }
1030
-
1031
- async function attemptErrorCorrection(
1032
- currentSql: string,
1033
- errorMessage: string,
1034
- question: string,
1035
- services: TDSServiceSchema[],
1036
- coordinates: string,
1037
- plugin: LegendAI_LegendApplicationPlugin_Extension,
1038
- config: LegendAIConfig,
1039
- ): Promise<string | undefined> {
1040
- const prompt = plugin.buildErrorCorrectionPrompt(
1041
- currentSql,
1042
- errorMessage,
1043
- question,
1044
- services,
1045
- coordinates,
1046
- );
1047
- if (!prompt) {
1048
- return undefined;
1049
- }
1050
- try {
1051
- const correctedSql = await plugin.callLLM(prompt, config);
1052
- const trimmed = cleanLlmSqlResponse(correctedSql);
1053
- if (isValidSqlCorrection(trimmed, currentSql)) {
1054
- return trimmed;
1055
- }
1056
- } catch {
1057
- /* empty */
592
+ finishWithThinkingError(setMessages, error.message, startTime);
1058
593
  }
1059
- return undefined;
1060
594
  }
1061
595
 
1062
- async function attemptZeroRowCorrection(
1063
- currentSql: string,
596
+ async function processDataQuery(
1064
597
  question: string,
1065
598
  services: TDSServiceSchema[],
1066
599
  coordinates: string,
1067
- dataProductCoordinates:
1068
- | LegendAIOrchestratorDataProductCoordinates
1069
- | undefined,
1070
- context: LegendAIOperationContext,
1071
- ): Promise<
1072
- { sql: string; result: LegendAISqlExecutionResultData } | undefined
1073
- > {
1074
- const { plugin, config, setMessages } = context;
1075
- addThinkingStep(
1076
- setMessages,
1077
- 'Query returned 0 rows, attempting filter correction...',
1078
- );
1079
- const prompt = plugin.buildZeroRowCorrectionPrompt(
1080
- currentSql,
1081
- question,
1082
- services,
1083
- coordinates,
1084
- );
1085
- if (!prompt) {
1086
- return undefined;
1087
- }
1088
- try {
1089
- const correctedSql = await plugin.callLLM(prompt, config);
1090
- const trimmed = cleanLlmSqlResponse(correctedSql);
1091
- if (!isValidSqlCorrection(trimmed, currentSql)) {
1092
- return undefined;
1093
- }
1094
- addThinkingStep(setMessages, 'Retrying with corrected filters...');
1095
- updateLastAssistant(setMessages, () => ({ sql: trimmed }));
1096
- try {
1097
- const retryResult = await executeSqlForServices(
1098
- trimmed,
1099
- services,
1100
- dataProductCoordinates,
1101
- plugin,
1102
- config,
1103
- );
1104
- if (retryResult.rows.length > 0) {
1105
- return { sql: trimmed, result: retryResult };
1106
- }
1107
- } catch {
1108
- /* empty */
1109
- }
1110
- } catch {
1111
- /* empty */
1112
- }
1113
- return undefined;
1114
- }
1115
-
1116
- function buildZeroRowMessage(services: TDSServiceSchema[]): string {
1117
- const withUnresolvable = services.filter((s) => hasUnresolvableParams(s));
1118
- if (withUnresolvable.length > 0) {
1119
- const parts: string[] = [];
1120
- for (const svc of withUnresolvable) {
1121
- for (const paramName of getNonDateParamNames(svc)) {
1122
- const matchingCol = svc.columns.find((c) => c.name === paramName);
1123
- const docHint = matchingCol?.documentation ?? matchingCol?.sampleValues;
1124
- if (docHint) {
1125
- parts.push(`**${paramName}** (${docHint})`);
1126
- } else {
1127
- parts.push(`**${paramName}**`);
1128
- }
1129
- }
1130
- }
1131
- const uniqueParts = [...new Set(parts)];
1132
- const firstSvc = withUnresolvable[0];
1133
- const firstParam = firstSvc
1134
- ? (getNonDateParamNames(firstSvc)[0] ?? 'parameter')
1135
- : 'parameter';
1136
- return `The SQL query executed successfully but returned **0 rows**. This service requires specific values for ${uniqueParts.join(', ')} to return data. Please include ${uniqueParts.length === 1 ? 'a value' : 'values'} in your question, e.g., "show data where ${firstParam} is [your value]".`;
1137
- }
1138
- return 'The SQL query executed successfully but returned **0 rows**. The applied filters may not match any records, or the specific values may not exist in the queried datasets.';
1139
- }
1140
-
1141
- function offerOrchestratorFallbackMessage(
1142
- setMessages: MessageSetter,
1143
- startTime: number,
1144
- fallbackMessage: string,
1145
- ): void {
1146
- updateLastAssistant(setMessages, () => ({
1147
- textAnswer: fallbackMessage,
1148
- fallbackAction: {
1149
- label: 'Try Legend AI Orchestrator',
1150
- actionId: LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
1151
- },
1152
- isProcessing: false,
1153
- thinkingDuration: elapsedSeconds(startTime),
1154
- }));
1155
- }
1156
-
1157
- function reportFatalQueryError(
1158
- setMessages: MessageSetter,
1159
- startTime: number,
1160
- errorMessage: string,
1161
- errorType: LegendAIErrorType,
1162
- ): void {
1163
- finishWithThinkingError(setMessages, errorMessage, startTime, errorType);
1164
- }
1165
-
1166
- function handleSqlGenerationFailure(
1167
- setMessages: MessageSetter,
1168
- startTime: number,
1169
- hasOrchestratorFallback: boolean,
1170
- orchestratorMessage: string,
1171
- errorMessage: string,
1172
- errorType: LegendAIErrorType,
1173
- ): void {
1174
- completeThinkingSteps(setMessages);
1175
- if (hasOrchestratorFallback) {
1176
- offerOrchestratorFallbackMessage(
1177
- setMessages,
1178
- startTime,
1179
- orchestratorMessage,
1180
- );
1181
- } else {
1182
- reportFatalQueryError(setMessages, startTime, errorMessage, errorType);
1183
- }
1184
- }
1185
-
1186
- interface QueryResultReport {
1187
- currentSql: string;
1188
- sqlResult: LegendAISqlExecutionResultData;
1189
- question: string;
1190
- services: TDSServiceSchema[];
1191
- }
1192
-
1193
- async function reportQueryResults(
1194
- report: QueryResultReport,
1195
600
  metadata: LegendAIProductMetadata,
1196
601
  context: LegendAIOperationContext,
1197
602
  startTime: number,
1198
- hasOrchestratorFallback: boolean,
603
+ orchestratorOptions?: LegendAIOrchestratorOptionsParam,
1199
604
  ): Promise<void> {
1200
- const { currentSql, sqlResult, question, services } = report;
1201
- const { setMessages } = context;
1202
- if (sqlResult.rows.length > 0) {
1203
- const columns = deduplicateColumns(sqlResult.columns);
1204
- const rows = sqlResult.rows;
1205
- completeThinkingSteps(setMessages);
1206
- addThinkingStep(
1207
- setMessages,
1208
- `Retrieved ${rows.length} row${rows.length === 1 ? '' : 's'}`,
1209
- );
1210
- completeThinkingSteps(setMessages);
1211
- updateLastAssistant(setMessages, () => ({
1212
- sql: currentSql,
1213
- gridData: {
1214
- columnDefs: buildColumnDefsFromNames(columns),
1215
- rowData: rows,
1216
- },
1217
- execTime: elapsedSeconds(startTime, 2),
1218
- isProcessing: true,
1219
- isExecuting: false,
1220
- thinkingDuration: elapsedSeconds(startTime),
1221
- }));
605
+ const { config, plugin, setMessages } = context;
606
+ const dataProductCoordinates = orchestratorOptions?.dataProductCoordinates;
607
+ const pureExecutionContext = orchestratorOptions?.pureExecutionContext;
1222
608
 
1223
- try {
1224
- await analyzeOrchestratorResults(
609
+ if (services.length === 0) {
610
+ if (config.orchestratorUrl && dataProductCoordinates) {
611
+ completeThinkingSteps(setMessages);
612
+ await processQuestionViaOrchestrator(
1225
613
  question,
1226
- currentSql,
1227
- sqlResult,
614
+ dataProductCoordinates,
1228
615
  metadata,
1229
616
  context,
1230
- startTime,
1231
- );
1232
- } catch {
1233
- /* empty */
1234
- } finally {
1235
- completeThinkingSteps(setMessages);
1236
- updateLastAssistant(setMessages, () => ({
1237
- isProcessing: false,
1238
- thinkingDuration: elapsedSeconds(startTime),
1239
- }));
1240
- }
1241
- } else {
1242
- addThinkingStep(
1243
- setMessages,
1244
- 'Query returned 0 rows after correction attempts.',
1245
- );
1246
- completeThinkingSteps(setMessages);
1247
- const fallback = hasOrchestratorFallback
1248
- ? {
1249
- fallbackAction: {
1250
- label: 'Try Legend AI Orchestrator',
1251
- actionId: LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
1252
- } as LegendAIFallbackAction,
1253
- }
1254
- : {};
1255
- updateLastAssistant(setMessages, () => ({
1256
- textAnswer: buildZeroRowMessage(services),
1257
- ...fallback,
1258
- isProcessing: false,
1259
- isExecuting: false,
1260
- thinkingDuration: elapsedSeconds(startTime),
1261
- }));
1262
- }
1263
- }
1264
-
1265
- async function selectBestServices(
1266
- question: string,
1267
- services: TDSServiceSchema[],
1268
- context: LegendAIOperationContext,
1269
- ): Promise<TDSServiceSchema[]> {
1270
- const { plugin, config, setMessages } = context;
1271
- if (services.length <= 1) {
1272
- return services;
1273
- }
1274
- try {
1275
- addThinkingStep(setMessages, 'Selecting best service for your query...');
1276
- return await plugin.selectRelevantServices(question, services, config);
1277
- } catch {
1278
- return services;
1279
- }
1280
- }
1281
-
1282
- async function tryRecoverZeroRows(
1283
- currentSql: string,
1284
- sqlResult: LegendAISqlExecutionResultData,
1285
- question: string,
1286
- selectedServices: TDSServiceSchema[],
1287
- coordinates: string,
1288
- dataProductCoordinates:
1289
- | LegendAIOrchestratorDataProductCoordinates
1290
- | undefined,
1291
- context: LegendAIOperationContext,
1292
- ): Promise<{ sql: string; result: LegendAISqlExecutionResultData }> {
1293
- const { plugin, config, setMessages } = context;
1294
- let recoveredSql = currentSql;
1295
- let recoveredResult = sqlResult;
1296
-
1297
- const strippedSql = stripNonDateServiceParams(recoveredSql);
1298
- if (strippedSql !== recoveredSql) {
1299
- addThinkingStep(
1300
- setMessages,
1301
- 'Trying query without guessed parameter values...',
1302
- );
1303
- try {
1304
- const strippedResult = await executeSqlForServices(
1305
- strippedSql,
1306
- selectedServices,
1307
- dataProductCoordinates,
1308
- plugin,
1309
- config,
617
+ pureExecutionContext,
1310
618
  );
1311
- if (strippedResult.rows.length > 0) {
1312
- recoveredSql = strippedSql;
1313
- recoveredResult = strippedResult;
1314
- updateLastAssistant(setMessages, () => ({ sql: strippedSql }));
1315
- }
1316
- } catch {
1317
- /* empty */
1318
- }
1319
- }
1320
-
1321
- if (recoveredResult.rows.length === 0) {
1322
- const correction = await attemptZeroRowCorrection(
1323
- recoveredSql,
1324
- question,
1325
- selectedServices,
1326
- coordinates,
1327
- dataProductCoordinates,
1328
- context,
1329
- );
1330
- if (correction) {
1331
- recoveredSql = correction.sql;
1332
- recoveredResult = correction.result;
619
+ return;
1333
620
  }
1334
- }
1335
-
1336
- return { sql: recoveredSql, result: recoveredResult };
1337
- }
1338
-
1339
- async function processDataQuery(
1340
- question: string,
1341
- services: TDSServiceSchema[],
1342
- coordinates: string,
1343
- metadata: LegendAIProductMetadata,
1344
- context: LegendAIOperationContext,
1345
- startTime: number,
1346
- orchestratorOptions?: LegendAIOrchestratorOptionsParam,
1347
- ): Promise<void> {
1348
- const { config, setMessages } = context;
1349
- const dataProductCoordinates = orchestratorOptions?.dataProductCoordinates;
1350
- const hasOrchestratorFallback = Boolean(
1351
- config.orchestratorUrl && dataProductCoordinates,
1352
- );
1353
-
1354
- if (services.length === 0) {
1355
- handleSqlGenerationFailure(
621
+ finishWithThinkingError(
1356
622
  setMessages,
1357
- startTime,
1358
- hasOrchestratorFallback,
1359
- 'No TDS services available for SQL querying. You can try the Legend AI Orchestrator to generate a Pure query instead.',
1360
623
  'No TDS services available for querying',
1361
- LegendAIErrorType.GENERAL,
624
+ startTime,
1362
625
  );
1363
626
  return;
1364
627
  }
1365
628
 
1366
629
  addThinkingStep(setMessages, 'Found relevant services to query');
1367
630
 
1368
- const selectedServices = await selectBestServices(
1369
- question,
1370
- services,
1371
- context,
1372
- );
1373
-
1374
631
  const judgedSql = await generateAndJudgeSql(
1375
632
  question,
1376
- selectedServices,
633
+ services,
1377
634
  coordinates,
1378
635
  context,
1379
636
  startTime,
1380
637
  );
1381
638
 
1382
639
  if (!judgedSql) {
1383
- addThinkingStep(
1384
- setMessages,
1385
- 'SQL generation could not produce a valid query.',
1386
- );
1387
- handleSqlGenerationFailure(
1388
- setMessages,
1389
- startTime,
1390
- hasOrchestratorFallback,
1391
- 'SQL generation could not handle this query. You can try the Legend AI Orchestrator to generate a Pure query instead.',
1392
- 'SQL generation could not handle this query. Try rephrasing your question.',
1393
- LegendAIErrorType.GENERATION,
1394
- );
640
+ if (config.orchestratorUrl && dataProductCoordinates) {
641
+ addThinkingStep(
642
+ setMessages,
643
+ 'SQL generation could not handle this query, trying Legend AI orchestrator...',
644
+ );
645
+ updateLastAssistant(setMessages, () => ({
646
+ error: null,
647
+ isProcessing: true,
648
+ }));
649
+ await processQuestionViaOrchestrator(
650
+ question,
651
+ dataProductCoordinates,
652
+ metadata,
653
+ context,
654
+ pureExecutionContext,
655
+ );
656
+ return;
657
+ }
1395
658
  return;
1396
659
  }
1397
660
 
1398
- const sqlGenTimeValue = elapsedSeconds(startTime, 2);
661
+ const sqlGenTimeValue = ((Date.now() - startTime) / 1000).toFixed(2);
1399
662
  completeThinkingSteps(setMessages);
1400
663
  updateLastAssistant(setMessages, () => ({
1401
664
  sql: judgedSql,
@@ -1403,76 +666,39 @@ async function processDataQuery(
1403
666
  isExecuting: true,
1404
667
  }));
1405
668
 
1406
- const execOutcome = await executeSqlWithRetries(
669
+ const sqlResult = await executeSqlAndReport(
1407
670
  judgedSql,
1408
- question,
1409
- selectedServices,
1410
- coordinates,
671
+ services,
672
+ config,
673
+ plugin,
674
+ setMessages,
675
+ startTime,
1411
676
  dataProductCoordinates,
1412
- context,
1413
677
  );
1414
678
 
1415
- if (execOutcome.error) {
1416
- const execErrorType = classifyError(new Error(execOutcome.error));
679
+ if (
680
+ sqlResult?.rows.length === 0 &&
681
+ config.orchestratorUrl &&
682
+ dataProductCoordinates
683
+ ) {
1417
684
  addThinkingStep(
1418
685
  setMessages,
1419
- `Execution failed: ${execOutcome.error.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
1420
- );
1421
- finishWithThinkingError(
1422
- setMessages,
1423
- buildExecutionErrorMessage(execOutcome.error, selectedServices),
1424
- startTime,
1425
- execErrorType === LegendAIErrorType.GENERAL
1426
- ? LegendAIErrorType.EXECUTION
1427
- : execErrorType,
686
+ 'SQL query returned no results, trying Legend AI orchestrator...',
1428
687
  );
1429
688
  updateLastAssistant(setMessages, () => ({
689
+ gridData: null,
690
+ error: null,
691
+ isProcessing: true,
1430
692
  isExecuting: false,
1431
- ...(hasOrchestratorFallback
1432
- ? {
1433
- fallbackAction: {
1434
- label: 'Try Legend AI Orchestrator',
1435
- actionId: LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
1436
- } as LegendAIFallbackAction,
1437
- }
1438
- : {}),
1439
693
  }));
1440
- return;
1441
- }
1442
-
1443
- if (!execOutcome.result) {
1444
- return;
1445
- }
1446
-
1447
- let currentSql = execOutcome.sql;
1448
- let sqlResult = execOutcome.result;
1449
-
1450
- if (sqlResult.rows.length === 0) {
1451
- const recovered = await tryRecoverZeroRows(
1452
- currentSql,
1453
- sqlResult,
694
+ await processQuestionViaOrchestrator(
1454
695
  question,
1455
- selectedServices,
1456
- coordinates,
1457
696
  dataProductCoordinates,
697
+ metadata,
1458
698
  context,
699
+ pureExecutionContext,
1459
700
  );
1460
- currentSql = recovered.sql;
1461
- sqlResult = recovered.result;
1462
701
  }
1463
-
1464
- await reportQueryResults(
1465
- {
1466
- currentSql,
1467
- sqlResult,
1468
- question,
1469
- services: selectedServices,
1470
- },
1471
- metadata,
1472
- context,
1473
- startTime,
1474
- hasOrchestratorFallback,
1475
- );
1476
702
  }
1477
703
 
1478
704
  export async function processQuestion(
@@ -1490,96 +716,66 @@ export async function processQuestion(
1490
716
  try {
1491
717
  addThinkingStep(setMessages, 'Analyzing your question...');
1492
718
 
1493
- const orchestratorOpts = dataProductCoordinates
1494
- ? {
1495
- dataProductCoordinates,
1496
- ...(pureExecutionContext === undefined
1497
- ? {}
1498
- : { pureExecutionContext }),
1499
- }
1500
- : undefined;
1501
-
1502
- if (services.length > 0) {
1503
- const serviceNames = services.map((s) => s.title);
1504
- const intent = await plugin.classifyQuestionIntent(
719
+ const serviceNames = services.map((s) => s.title);
720
+ const intent = await plugin.classifyQuestionIntent(
721
+ question,
722
+ services.length > 0,
723
+ config,
724
+ serviceNames,
725
+ );
726
+
727
+ if (intent === LegendAIQuestionIntent.METADATA) {
728
+ await handleMetadataQuestion(
1505
729
  question,
1506
- true,
1507
- config,
1508
- serviceNames,
730
+ metadata,
731
+ context,
732
+ startTime,
733
+ services.length > 0,
1509
734
  );
735
+ return;
736
+ }
1510
737
 
1511
- if (intent === LegendAIQuestionIntent.METADATA) {
1512
- await handleMetadataQuestion(
738
+ if (intent === LegendAIQuestionIntent.ORCHESTRATOR) {
739
+ if (config.orchestratorUrl && dataProductCoordinates) {
740
+ completeThinkingSteps(setMessages);
741
+ await processQuestionViaOrchestrator(
1513
742
  question,
743
+ dataProductCoordinates,
1514
744
  metadata,
1515
745
  context,
1516
- startTime,
1517
- true,
746
+ pureExecutionContext,
1518
747
  );
1519
748
  return;
1520
749
  }
1521
-
1522
- // DATA_QUERY or ORCHESTRATOR — try SQL generation.
1523
- // If SQL throws, fall back to metadata as a safety net
1524
- // (e.g. misclassified metadata question).
1525
- try {
1526
- await processDataQuery(
1527
- question,
1528
- services,
1529
- coordinates,
1530
- metadata,
1531
- context,
1532
- startTime,
1533
- orchestratorOpts,
1534
- );
1535
- } catch (sqlError) {
1536
- assertErrorThrown(sqlError);
1537
- addThinkingStep(
1538
- setMessages,
1539
- 'SQL generation failed, answering from product metadata...',
1540
- );
1541
- await handleMetadataQuestion(
1542
- question,
1543
- metadata,
1544
- context,
1545
- startTime,
1546
- true,
1547
- );
1548
- }
1549
- return;
1550
- }
1551
-
1552
- // No services available — use orchestrator if configured, else metadata only.
1553
- if (config.orchestratorUrl && dataProductCoordinates) {
1554
- completeThinkingSteps(setMessages);
1555
- await processQuestionViaOrchestrator(
1556
- question,
1557
- dataProductCoordinates,
1558
- metadata,
1559
- context,
1560
- pureExecutionContext,
1561
- );
1562
- } else {
1563
- await handleMetadataQuestion(
1564
- question,
1565
- metadata,
1566
- context,
1567
- startTime,
1568
- false,
750
+ addThinkingStep(
751
+ setMessages,
752
+ 'Orchestrator not available, trying SQL generation...',
1569
753
  );
1570
754
  }
755
+
756
+ await processDataQuery(
757
+ question,
758
+ services,
759
+ coordinates,
760
+ metadata,
761
+ context,
762
+ startTime,
763
+ dataProductCoordinates
764
+ ? {
765
+ dataProductCoordinates,
766
+ ...(pureExecutionContext === undefined
767
+ ? {}
768
+ : { pureExecutionContext }),
769
+ }
770
+ : undefined,
771
+ );
1571
772
  } catch (error) {
1572
773
  assertErrorThrown(error);
1573
774
  addThinkingStep(
1574
775
  setMessages,
1575
776
  `Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
1576
777
  );
1577
- finishWithThinkingError(
1578
- setMessages,
1579
- error.message,
1580
- startTime,
1581
- classifyError(error),
1582
- );
778
+ finishWithThinkingError(setMessages, error.message, startTime);
1583
779
  }
1584
780
  }
1585
781
 
@@ -1598,61 +794,18 @@ export async function processQuestionWithIntent(
1598
794
 
1599
795
  if (intent === LegendAIQuestionIntent.METADATA) {
1600
796
  const startTime = Date.now();
1601
- try {
1602
- await handleMetadataQuestion(
1603
- question,
1604
- metadata,
1605
- context,
1606
- startTime,
1607
- services.length > 0,
1608
- );
1609
- } catch (error) {
1610
- assertErrorThrown(error);
1611
- addThinkingStep(
1612
- setMessages,
1613
- `Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
1614
- );
1615
- finishWithThinkingError(
1616
- setMessages,
1617
- error.message,
1618
- startTime,
1619
- classifyError(error),
1620
- );
1621
- }
797
+ await handleMetadataQuestion(
798
+ question,
799
+ metadata,
800
+ context,
801
+ startTime,
802
+ services.length > 0,
803
+ );
1622
804
  return;
1623
805
  }
1624
806
 
1625
807
  if (intent === LegendAIQuestionIntent.ORCHESTRATOR) {
1626
808
  if (config.orchestratorUrl && dataProductCoordinates) {
1627
- // When services are available, try SQL first even for ORCHESTRATOR intent
1628
- if (services.length > 0) {
1629
- const startTime = Date.now();
1630
- try {
1631
- addThinkingStep(setMessages, 'Preparing data query...');
1632
- await processDataQuery(
1633
- question,
1634
- services,
1635
- coordinates,
1636
- metadata,
1637
- context,
1638
- startTime,
1639
- orchestratorOptions,
1640
- );
1641
- } catch (error) {
1642
- assertErrorThrown(error);
1643
- addThinkingStep(
1644
- setMessages,
1645
- `Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
1646
- );
1647
- finishWithThinkingError(
1648
- setMessages,
1649
- error.message,
1650
- startTime,
1651
- classifyError(error),
1652
- );
1653
- }
1654
- return;
1655
- }
1656
809
  await processQuestionViaOrchestrator(
1657
810
  question,
1658
811
  dataProductCoordinates,
@@ -1683,12 +836,7 @@ export async function processQuestionWithIntent(
1683
836
  setMessages,
1684
837
  `Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
1685
838
  );
1686
- finishWithThinkingError(
1687
- setMessages,
1688
- error.message,
1689
- startTime,
1690
- classifyError(error),
1691
- );
839
+ finishWithThinkingError(setMessages, error.message, startTime);
1692
840
  }
1693
841
  }
1694
842
 
@@ -1841,64 +989,6 @@ export const useLegendAIChatState = (
1841
989
  ],
1842
990
  );
1843
991
 
1844
- const runFallbackAction = useCallback(
1845
- (messageId: string): void => {
1846
- if (isSending || !config.orchestratorUrl || !dataProductCoordinates) {
1847
- return;
1848
- }
1849
- // Find the user question associated with this assistant message
1850
- let question: string | undefined;
1851
- for (let i = 0; i < messages.length; i++) {
1852
- const msg = messages[i];
1853
- if (
1854
- msg?.role === LegendAIMessageRole.ASSISTANT &&
1855
- msg.id === messageId &&
1856
- i > 0
1857
- ) {
1858
- const userMsg = messages[i - 1];
1859
- if (userMsg?.role === LegendAIMessageRole.USER) {
1860
- question = userMsg.text;
1861
- }
1862
- }
1863
- }
1864
- if (!question) {
1865
- return;
1866
- }
1867
-
1868
- setIsSending(true);
1869
- setMessages((prev) =>
1870
- prev.map((m) =>
1871
- m.id === messageId && m.role === LegendAIMessageRole.ASSISTANT
1872
- ? { ...m, fallbackAction: null, error: null, isProcessing: true }
1873
- : m,
1874
- ),
1875
- );
1876
-
1877
- const history = buildConversationHistory(messages);
1878
- const q = question;
1879
- processQuestionViaOrchestrator(
1880
- q,
1881
- dataProductCoordinates,
1882
- metadata,
1883
- { config, plugin, history, setMessages },
1884
- pureExecutionContext,
1885
- )
1886
- .catch(noop())
1887
- .finally(() => {
1888
- setIsSending(false);
1889
- });
1890
- },
1891
- [
1892
- isSending,
1893
- messages,
1894
- config,
1895
- metadata,
1896
- plugin,
1897
- dataProductCoordinates,
1898
- pureExecutionContext,
1899
- ],
1900
- );
1901
-
1902
992
  return {
1903
993
  questionText,
1904
994
  setQuestionText,
@@ -1906,7 +996,6 @@ export const useLegendAIChatState = (
1906
996
  messages,
1907
997
  askQuestion,
1908
998
  askQuestionWithIntent,
1909
- runFallbackAction,
1910
999
  clearChat,
1911
1000
  expandedThinking,
1912
1001
  toggleThinking,