@finos/legend-lego 2.0.195 → 2.0.197
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/code-editor/CodeEditor.d.ts.map +1 -1
- package/lib/code-editor/CodeEditor.js +14 -1
- package/lib/code-editor/CodeEditor.js.map +1 -1
- package/lib/index.css +2 -2
- package/lib/index.css.map +1 -1
- package/lib/legend-ai/LegendAITypes.d.ts +33 -0
- package/lib/legend-ai/LegendAITypes.d.ts.map +1 -1
- package/lib/legend-ai/LegendAITypes.js +39 -1
- package/lib/legend-ai/LegendAITypes.js.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts +96 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js +56 -0
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js.map +1 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.d.ts.map +1 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.js +6 -0
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.js.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisPanel.d.ts +24 -0
- package/lib/legend-ai/components/LegendAIAnalysisPanel.d.ts.map +1 -0
- package/lib/legend-ai/components/LegendAIAnalysisPanel.js +35 -0
- package/lib/legend-ai/components/LegendAIAnalysisPanel.js.map +1 -0
- package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts +23 -0
- package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts.map +1 -0
- package/lib/legend-ai/components/LegendAIAnalysisUtils.js +168 -0
- package/lib/legend-ai/components/LegendAIAnalysisUtils.js.map +1 -0
- package/lib/legend-ai/components/LegendAICharts.d.ts +25 -0
- package/lib/legend-ai/components/LegendAICharts.d.ts.map +1 -0
- package/lib/legend-ai/components/LegendAICharts.js +70 -0
- package/lib/legend-ai/components/LegendAICharts.js.map +1 -0
- package/lib/legend-ai/components/LegendAIChat.d.ts +2 -1
- package/lib/legend-ai/components/LegendAIChat.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIChat.js +14 -10
- package/lib/legend-ai/components/LegendAIChat.js.map +1 -1
- package/lib/legend-ai/index.d.ts +5 -2
- package/lib/legend-ai/index.d.ts.map +1 -1
- package/lib/legend-ai/index.js +5 -2
- package/lib/legend-ai/index.js.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatState.d.ts +12 -5
- package/lib/legend-ai/stores/LegendAIChatState.d.ts.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatState.js +604 -69
- package/lib/legend-ai/stores/LegendAIChatState.js.map +1 -1
- package/package.json +5 -5
- package/src/code-editor/CodeEditor.tsx +19 -0
- package/src/legend-ai/LegendAITypes.ts +51 -1
- package/src/legend-ai/LegendAI_LegendApplicationPlugin_Extension.ts +169 -0
- package/src/legend-ai/__test-utils__/LegendAITestUtils.ts +9 -0
- package/src/legend-ai/components/LegendAIAnalysisPanel.tsx +102 -0
- package/src/legend-ai/components/LegendAIAnalysisUtils.ts +226 -0
- package/src/legend-ai/components/LegendAICharts.tsx +166 -0
- package/src/legend-ai/components/LegendAIChat.tsx +74 -26
- package/src/legend-ai/index.ts +18 -0
- package/src/legend-ai/stores/LegendAIChatState.ts +1039 -128
- package/tsconfig.json +3 -0
|
@@ -25,16 +25,21 @@ import {
|
|
|
25
25
|
type LegendAIMessage,
|
|
26
26
|
type LegendAIConversationTurn,
|
|
27
27
|
type LegendAIProductMetadata,
|
|
28
|
+
type LegendAIFallbackAction,
|
|
28
29
|
LegendAIQuestionIntent,
|
|
29
30
|
LegendAIThinkingStepStatus,
|
|
30
31
|
LegendAIMessageRole,
|
|
32
|
+
LegendAIErrorType,
|
|
33
|
+
LegendAIServiceError,
|
|
31
34
|
TDSServiceSourceType,
|
|
32
35
|
buildColumnDefsFromNames,
|
|
36
|
+
LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
|
|
33
37
|
} from '../LegendAITypes.js';
|
|
34
38
|
import {
|
|
35
39
|
type LegendAI_LegendApplicationPlugin_Extension,
|
|
36
40
|
type LegendAIOrchestratorDataProductCoordinates,
|
|
37
41
|
type LegendAISqlExecutionResultData,
|
|
42
|
+
type LegendAIResolvedEntities,
|
|
38
43
|
LegendAIJudgeVerdict,
|
|
39
44
|
} from '../LegendAI_LegendApplicationPlugin_Extension.js';
|
|
40
45
|
import type { QueryExplicitExecutionContextInfo } from '@finos/legend-graph';
|
|
@@ -42,9 +47,32 @@ import type { QueryExplicitExecutionContextInfo } from '@finos/legend-graph';
|
|
|
42
47
|
const MAX_ERROR_MESSAGE_LENGTH = 500;
|
|
43
48
|
const MAX_THINKING_ERROR_PREVIEW_LENGTH = 200;
|
|
44
49
|
const DEFAULT_MAX_JUDGE_ATTEMPTS = 5;
|
|
50
|
+
const DEFAULT_MAX_EXECUTION_RETRIES = 3;
|
|
51
|
+
const ANALYSIS_TIMEOUT_MS = 15_000;
|
|
45
52
|
|
|
46
53
|
const SUGGESTED_QUERIES_DELIMITER = '---SUGGESTED_QUERIES---';
|
|
47
54
|
|
|
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
|
+
|
|
48
76
|
function deduplicateColumns(columns: string[]): string[] {
|
|
49
77
|
const seen = new Map<string, number>();
|
|
50
78
|
return columns.map((col) => {
|
|
@@ -58,7 +86,7 @@ export type MessageSetter = React.Dispatch<
|
|
|
58
86
|
React.SetStateAction<LegendAIMessage[]>
|
|
59
87
|
>;
|
|
60
88
|
|
|
61
|
-
function createMessagePair(
|
|
89
|
+
export function createMessagePair(
|
|
62
90
|
text: string,
|
|
63
91
|
): [LegendAIUserMessage, LegendAIAssistantMessage] {
|
|
64
92
|
return [
|
|
@@ -69,19 +97,22 @@ function createMessagePair(
|
|
|
69
97
|
thinkingSteps: [],
|
|
70
98
|
sql: null,
|
|
71
99
|
textAnswer: null,
|
|
100
|
+
dataContext: null,
|
|
72
101
|
gridData: null,
|
|
73
102
|
error: null,
|
|
103
|
+
errorType: null,
|
|
74
104
|
sqlGenTime: null,
|
|
75
105
|
execTime: null,
|
|
76
106
|
thinkingDuration: null,
|
|
77
107
|
isProcessing: true,
|
|
78
108
|
isExecuting: false,
|
|
79
109
|
suggestedQueries: [],
|
|
110
|
+
fallbackAction: null,
|
|
80
111
|
},
|
|
81
112
|
];
|
|
82
113
|
}
|
|
83
114
|
|
|
84
|
-
interface LegendAIOperationContext {
|
|
115
|
+
export interface LegendAIOperationContext {
|
|
85
116
|
config: LegendAIConfig;
|
|
86
117
|
plugin: LegendAI_LegendApplicationPlugin_Extension;
|
|
87
118
|
history: LegendAIConversationTurn[];
|
|
@@ -119,7 +150,7 @@ export function addThinkingStep(
|
|
|
119
150
|
? { ...s, status: LegendAIThinkingStepStatus.DONE }
|
|
120
151
|
: s,
|
|
121
152
|
),
|
|
122
|
-
{ label, status: LegendAIThinkingStepStatus.ACTIVE },
|
|
153
|
+
{ id: uuid(), label, status: LegendAIThinkingStepStatus.ACTIVE },
|
|
123
154
|
],
|
|
124
155
|
}));
|
|
125
156
|
}
|
|
@@ -134,10 +165,18 @@ export function completeThinkingSteps(setMessages: MessageSetter): void {
|
|
|
134
165
|
}));
|
|
135
166
|
}
|
|
136
167
|
|
|
168
|
+
export function classifyError(error: Error): LegendAIErrorType {
|
|
169
|
+
if (error instanceof LegendAIServiceError) {
|
|
170
|
+
return error.errorType;
|
|
171
|
+
}
|
|
172
|
+
return LegendAIErrorType.GENERAL;
|
|
173
|
+
}
|
|
174
|
+
|
|
137
175
|
export function finishWithThinkingError(
|
|
138
176
|
setMessages: MessageSetter,
|
|
139
177
|
errorMsg: string,
|
|
140
178
|
startTime: number,
|
|
179
|
+
errorType?: LegendAIErrorType,
|
|
141
180
|
): void {
|
|
142
181
|
updateLastAssistant(setMessages, (msg) => ({
|
|
143
182
|
thinkingSteps: msg.thinkingSteps.map((s) =>
|
|
@@ -146,8 +185,9 @@ export function finishWithThinkingError(
|
|
|
146
185
|
: s,
|
|
147
186
|
),
|
|
148
187
|
error: errorMsg.slice(0, MAX_ERROR_MESSAGE_LENGTH),
|
|
188
|
+
errorType: errorType ?? null,
|
|
149
189
|
isProcessing: false,
|
|
150
|
-
thinkingDuration: (
|
|
190
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
151
191
|
}));
|
|
152
192
|
}
|
|
153
193
|
|
|
@@ -306,7 +346,7 @@ export async function handleMetadataQuestion(
|
|
|
306
346
|
textAnswer: answer,
|
|
307
347
|
suggestedQueries,
|
|
308
348
|
isProcessing: false,
|
|
309
|
-
thinkingDuration: (
|
|
349
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
310
350
|
}));
|
|
311
351
|
}
|
|
312
352
|
|
|
@@ -340,6 +380,7 @@ export async function generateAndJudgeSql(
|
|
|
340
380
|
setMessages,
|
|
341
381
|
buildGenerationFailureMessage(failure, suggestion, services),
|
|
342
382
|
startTime,
|
|
383
|
+
LegendAIErrorType.GENERATION,
|
|
343
384
|
);
|
|
344
385
|
return null;
|
|
345
386
|
}
|
|
@@ -350,6 +391,7 @@ export async function generateAndJudgeSql(
|
|
|
350
391
|
setMessages,
|
|
351
392
|
'Could not extract SQL from LLM response.\nTry rephrasing your question or ask about a specific service.',
|
|
352
393
|
startTime,
|
|
394
|
+
LegendAIErrorType.GENERATION,
|
|
353
395
|
);
|
|
354
396
|
return null;
|
|
355
397
|
}
|
|
@@ -419,10 +461,10 @@ function reportExecutionResult(
|
|
|
419
461
|
|
|
420
462
|
updateLastAssistant(setMessages, () => ({
|
|
421
463
|
gridData: { columnDefs: buildColumnDefsFromNames(columns), rowData: rows },
|
|
422
|
-
execTime: (
|
|
464
|
+
execTime: elapsedSeconds(execStartTime, 2),
|
|
423
465
|
isProcessing: false,
|
|
424
466
|
isExecuting: false,
|
|
425
|
-
thinkingDuration: (
|
|
467
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
426
468
|
}));
|
|
427
469
|
return { columns, rows };
|
|
428
470
|
}
|
|
@@ -453,6 +495,7 @@ export async function executeSqlAndReport(
|
|
|
453
495
|
);
|
|
454
496
|
} catch (executeError) {
|
|
455
497
|
assertErrorThrown(executeError);
|
|
498
|
+
const execErrorType = classifyError(executeError);
|
|
456
499
|
addThinkingStep(
|
|
457
500
|
setMessages,
|
|
458
501
|
`Execution failed: ${executeError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
@@ -461,9 +504,12 @@ export async function executeSqlAndReport(
|
|
|
461
504
|
setMessages,
|
|
462
505
|
buildExecutionErrorMessage(executeError.message, services),
|
|
463
506
|
startTime,
|
|
507
|
+
execErrorType === LegendAIErrorType.GENERAL
|
|
508
|
+
? LegendAIErrorType.EXECUTION
|
|
509
|
+
: execErrorType,
|
|
464
510
|
);
|
|
465
511
|
updateLastAssistant(setMessages, () => ({
|
|
466
|
-
execTime: (
|
|
512
|
+
execTime: elapsedSeconds(execStartTime, 2),
|
|
467
513
|
isExecuting: false,
|
|
468
514
|
}));
|
|
469
515
|
return undefined;
|
|
@@ -496,44 +542,183 @@ export async function executePureQueryAndReport(
|
|
|
496
542
|
);
|
|
497
543
|
} catch (executeError) {
|
|
498
544
|
assertErrorThrown(executeError);
|
|
545
|
+
const execErrorType = classifyError(executeError);
|
|
499
546
|
addThinkingStep(
|
|
500
547
|
setMessages,
|
|
501
548
|
`Execution failed: ${executeError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
502
549
|
);
|
|
503
550
|
completeThinkingSteps(setMessages);
|
|
504
551
|
updateLastAssistant(setMessages, () => ({
|
|
505
|
-
execTime: (
|
|
552
|
+
execTime: elapsedSeconds(execStartTime, 2),
|
|
506
553
|
isExecuting: false,
|
|
507
554
|
isProcessing: false,
|
|
508
555
|
error: `Execution failed: ${executeError.message.slice(0, MAX_ERROR_MESSAGE_LENGTH)}`,
|
|
509
|
-
|
|
556
|
+
errorType:
|
|
557
|
+
execErrorType === LegendAIErrorType.GENERAL
|
|
558
|
+
? LegendAIErrorType.EXECUTION
|
|
559
|
+
: execErrorType,
|
|
560
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
510
561
|
}));
|
|
511
562
|
return { columns: [], rows: [] };
|
|
512
563
|
}
|
|
513
564
|
}
|
|
514
565
|
|
|
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
|
+
|
|
515
689
|
export async function processQuestionViaOrchestrator(
|
|
516
690
|
question: string,
|
|
517
691
|
dataProductCoordinates: LegendAIOrchestratorDataProductCoordinates,
|
|
518
|
-
|
|
692
|
+
metadata: LegendAIProductMetadata,
|
|
519
693
|
context: LegendAIOperationContext,
|
|
520
694
|
pureExecutionContext?: QueryExplicitExecutionContextInfo,
|
|
695
|
+
preResolvedEntities?: LegendAIResolvedEntities,
|
|
521
696
|
): Promise<void> {
|
|
522
697
|
const { config, plugin, setMessages } = context;
|
|
523
698
|
const startTime = Date.now();
|
|
524
699
|
|
|
525
700
|
try {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
+
}
|
|
532
721
|
|
|
533
|
-
addThinkingStep(
|
|
534
|
-
setMessages,
|
|
535
|
-
`Found root entity: ${resolvedEntities.rootEntity.split('::').pop() ?? resolvedEntities.rootEntity}`,
|
|
536
|
-
);
|
|
537
722
|
if (resolvedEntities.relatedEntities.length > 0) {
|
|
538
723
|
addThinkingStep(
|
|
539
724
|
setMessages,
|
|
@@ -554,7 +739,7 @@ export async function processQuestionViaOrchestrator(
|
|
|
554
739
|
config,
|
|
555
740
|
);
|
|
556
741
|
|
|
557
|
-
const queryGenTime = (
|
|
742
|
+
const queryGenTime = elapsedSeconds(startTime, 2);
|
|
558
743
|
completeThinkingSteps(setMessages);
|
|
559
744
|
updateLastAssistant(setMessages, () => ({
|
|
560
745
|
sql: orchestratorResponse.legend_query,
|
|
@@ -569,12 +754,13 @@ export async function processQuestionViaOrchestrator(
|
|
|
569
754
|
isExecuting: false,
|
|
570
755
|
error:
|
|
571
756
|
'No execution context available — cannot execute query via engine.',
|
|
572
|
-
|
|
757
|
+
errorType: LegendAIErrorType.EXECUTION,
|
|
758
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
573
759
|
}));
|
|
574
760
|
return;
|
|
575
761
|
}
|
|
576
762
|
|
|
577
|
-
await executePureQueryAndReport(
|
|
763
|
+
const execResult = await executePureQueryAndReport(
|
|
578
764
|
orchestratorResponse.legend_query,
|
|
579
765
|
pureExecutionContext,
|
|
580
766
|
dataProductCoordinates,
|
|
@@ -583,82 +769,633 @@ export async function processQuestionViaOrchestrator(
|
|
|
583
769
|
setMessages,
|
|
584
770
|
startTime,
|
|
585
771
|
);
|
|
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
|
+
}
|
|
586
803
|
} catch (error) {
|
|
587
804
|
assertErrorThrown(error);
|
|
805
|
+
const orchErrorType = classifyError(error);
|
|
588
806
|
addThinkingStep(
|
|
589
807
|
setMessages,
|
|
590
808
|
`Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
591
809
|
);
|
|
592
|
-
|
|
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
|
+
);
|
|
593
839
|
}
|
|
594
840
|
}
|
|
595
841
|
|
|
596
|
-
|
|
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 */
|
|
1058
|
+
}
|
|
1059
|
+
return undefined;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
async function attemptZeroRowCorrection(
|
|
1063
|
+
currentSql: string,
|
|
597
1064
|
question: string,
|
|
598
1065
|
services: TDSServiceSchema[],
|
|
599
1066
|
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,
|
|
600
1195
|
metadata: LegendAIProductMetadata,
|
|
601
1196
|
context: LegendAIOperationContext,
|
|
602
1197
|
startTime: number,
|
|
603
|
-
|
|
1198
|
+
hasOrchestratorFallback: boolean,
|
|
604
1199
|
): Promise<void> {
|
|
605
|
-
const {
|
|
606
|
-
const
|
|
607
|
-
|
|
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
|
+
}));
|
|
608
1222
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
completeThinkingSteps(setMessages);
|
|
612
|
-
await processQuestionViaOrchestrator(
|
|
1223
|
+
try {
|
|
1224
|
+
await analyzeOrchestratorResults(
|
|
613
1225
|
question,
|
|
614
|
-
|
|
1226
|
+
currentSql,
|
|
1227
|
+
sqlResult,
|
|
615
1228
|
metadata,
|
|
616
1229
|
context,
|
|
617
|
-
|
|
1230
|
+
startTime,
|
|
618
1231
|
);
|
|
619
|
-
|
|
1232
|
+
} catch {
|
|
1233
|
+
/* empty */
|
|
1234
|
+
} finally {
|
|
1235
|
+
completeThinkingSteps(setMessages);
|
|
1236
|
+
updateLastAssistant(setMessages, () => ({
|
|
1237
|
+
isProcessing: false,
|
|
1238
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
1239
|
+
}));
|
|
620
1240
|
}
|
|
621
|
-
|
|
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,
|
|
1310
|
+
);
|
|
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;
|
|
1333
|
+
}
|
|
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(
|
|
622
1356
|
setMessages,
|
|
623
|
-
'No TDS services available for querying',
|
|
624
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
|
+
'No TDS services available for querying',
|
|
1361
|
+
LegendAIErrorType.GENERAL,
|
|
625
1362
|
);
|
|
626
1363
|
return;
|
|
627
1364
|
}
|
|
628
1365
|
|
|
629
1366
|
addThinkingStep(setMessages, 'Found relevant services to query');
|
|
630
1367
|
|
|
631
|
-
const
|
|
1368
|
+
const selectedServices = await selectBestServices(
|
|
632
1369
|
question,
|
|
633
1370
|
services,
|
|
1371
|
+
context,
|
|
1372
|
+
);
|
|
1373
|
+
|
|
1374
|
+
const judgedSql = await generateAndJudgeSql(
|
|
1375
|
+
question,
|
|
1376
|
+
selectedServices,
|
|
634
1377
|
coordinates,
|
|
635
1378
|
context,
|
|
636
1379
|
startTime,
|
|
637
1380
|
);
|
|
638
1381
|
|
|
639
1382
|
if (!judgedSql) {
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
metadata,
|
|
653
|
-
context,
|
|
654
|
-
pureExecutionContext,
|
|
655
|
-
);
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
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
|
+
);
|
|
658
1395
|
return;
|
|
659
1396
|
}
|
|
660
1397
|
|
|
661
|
-
const sqlGenTimeValue = (
|
|
1398
|
+
const sqlGenTimeValue = elapsedSeconds(startTime, 2);
|
|
662
1399
|
completeThinkingSteps(setMessages);
|
|
663
1400
|
updateLastAssistant(setMessages, () => ({
|
|
664
1401
|
sql: judgedSql,
|
|
@@ -666,39 +1403,76 @@ async function processDataQuery(
|
|
|
666
1403
|
isExecuting: true,
|
|
667
1404
|
}));
|
|
668
1405
|
|
|
669
|
-
const
|
|
1406
|
+
const execOutcome = await executeSqlWithRetries(
|
|
670
1407
|
judgedSql,
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
setMessages,
|
|
675
|
-
startTime,
|
|
1408
|
+
question,
|
|
1409
|
+
selectedServices,
|
|
1410
|
+
coordinates,
|
|
676
1411
|
dataProductCoordinates,
|
|
1412
|
+
context,
|
|
677
1413
|
);
|
|
678
1414
|
|
|
679
|
-
if (
|
|
680
|
-
|
|
681
|
-
config.orchestratorUrl &&
|
|
682
|
-
dataProductCoordinates
|
|
683
|
-
) {
|
|
1415
|
+
if (execOutcome.error) {
|
|
1416
|
+
const execErrorType = classifyError(new Error(execOutcome.error));
|
|
684
1417
|
addThinkingStep(
|
|
685
1418
|
setMessages,
|
|
686
|
-
|
|
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,
|
|
687
1428
|
);
|
|
688
1429
|
updateLastAssistant(setMessages, () => ({
|
|
689
|
-
gridData: null,
|
|
690
|
-
error: null,
|
|
691
|
-
isProcessing: true,
|
|
692
1430
|
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
|
+
: {}),
|
|
693
1439
|
}));
|
|
694
|
-
|
|
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,
|
|
695
1454
|
question,
|
|
1455
|
+
selectedServices,
|
|
1456
|
+
coordinates,
|
|
696
1457
|
dataProductCoordinates,
|
|
697
|
-
metadata,
|
|
698
1458
|
context,
|
|
699
|
-
pureExecutionContext,
|
|
700
1459
|
);
|
|
1460
|
+
currentSql = recovered.sql;
|
|
1461
|
+
sqlResult = recovered.result;
|
|
701
1462
|
}
|
|
1463
|
+
|
|
1464
|
+
await reportQueryResults(
|
|
1465
|
+
{
|
|
1466
|
+
currentSql,
|
|
1467
|
+
sqlResult,
|
|
1468
|
+
question,
|
|
1469
|
+
services: selectedServices,
|
|
1470
|
+
},
|
|
1471
|
+
metadata,
|
|
1472
|
+
context,
|
|
1473
|
+
startTime,
|
|
1474
|
+
hasOrchestratorFallback,
|
|
1475
|
+
);
|
|
702
1476
|
}
|
|
703
1477
|
|
|
704
1478
|
export async function processQuestion(
|
|
@@ -716,66 +1490,96 @@ export async function processQuestion(
|
|
|
716
1490
|
try {
|
|
717
1491
|
addThinkingStep(setMessages, 'Analyzing your question...');
|
|
718
1492
|
|
|
719
|
-
const
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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(
|
|
729
1505
|
question,
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
services.length > 0,
|
|
1506
|
+
true,
|
|
1507
|
+
config,
|
|
1508
|
+
serviceNames,
|
|
734
1509
|
);
|
|
735
|
-
return;
|
|
736
|
-
}
|
|
737
1510
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
completeThinkingSteps(setMessages);
|
|
741
|
-
await processQuestionViaOrchestrator(
|
|
1511
|
+
if (intent === LegendAIQuestionIntent.METADATA) {
|
|
1512
|
+
await handleMetadataQuestion(
|
|
742
1513
|
question,
|
|
743
|
-
dataProductCoordinates,
|
|
744
1514
|
metadata,
|
|
745
1515
|
context,
|
|
746
|
-
|
|
1516
|
+
startTime,
|
|
1517
|
+
true,
|
|
747
1518
|
);
|
|
748
1519
|
return;
|
|
749
1520
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
)
|
|
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;
|
|
754
1550
|
}
|
|
755
1551
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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,
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
772
1571
|
} catch (error) {
|
|
773
1572
|
assertErrorThrown(error);
|
|
774
1573
|
addThinkingStep(
|
|
775
1574
|
setMessages,
|
|
776
1575
|
`Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
777
1576
|
);
|
|
778
|
-
finishWithThinkingError(
|
|
1577
|
+
finishWithThinkingError(
|
|
1578
|
+
setMessages,
|
|
1579
|
+
error.message,
|
|
1580
|
+
startTime,
|
|
1581
|
+
classifyError(error),
|
|
1582
|
+
);
|
|
779
1583
|
}
|
|
780
1584
|
}
|
|
781
1585
|
|
|
@@ -794,18 +1598,61 @@ export async function processQuestionWithIntent(
|
|
|
794
1598
|
|
|
795
1599
|
if (intent === LegendAIQuestionIntent.METADATA) {
|
|
796
1600
|
const startTime = Date.now();
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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
|
+
}
|
|
804
1622
|
return;
|
|
805
1623
|
}
|
|
806
1624
|
|
|
807
1625
|
if (intent === LegendAIQuestionIntent.ORCHESTRATOR) {
|
|
808
1626
|
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
|
+
}
|
|
809
1656
|
await processQuestionViaOrchestrator(
|
|
810
1657
|
question,
|
|
811
1658
|
dataProductCoordinates,
|
|
@@ -836,7 +1683,12 @@ export async function processQuestionWithIntent(
|
|
|
836
1683
|
setMessages,
|
|
837
1684
|
`Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
838
1685
|
);
|
|
839
|
-
finishWithThinkingError(
|
|
1686
|
+
finishWithThinkingError(
|
|
1687
|
+
setMessages,
|
|
1688
|
+
error.message,
|
|
1689
|
+
startTime,
|
|
1690
|
+
classifyError(error),
|
|
1691
|
+
);
|
|
840
1692
|
}
|
|
841
1693
|
}
|
|
842
1694
|
|
|
@@ -989,6 +1841,64 @@ export const useLegendAIChatState = (
|
|
|
989
1841
|
],
|
|
990
1842
|
);
|
|
991
1843
|
|
|
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
|
+
|
|
992
1902
|
return {
|
|
993
1903
|
questionText,
|
|
994
1904
|
setQuestionText,
|
|
@@ -996,6 +1906,7 @@ export const useLegendAIChatState = (
|
|
|
996
1906
|
messages,
|
|
997
1907
|
askQuestion,
|
|
998
1908
|
askQuestionWithIntent,
|
|
1909
|
+
runFallbackAction,
|
|
999
1910
|
clearChat,
|
|
1000
1911
|
expandedThinking,
|
|
1001
1912
|
toggleThinking,
|