@finos/legend-lego 2.0.196 → 2.0.198

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 (71) hide show
  1. package/lib/code-editor/CodeEditor.d.ts.map +1 -1
  2. package/lib/code-editor/CodeEditor.js +14 -1
  3. package/lib/code-editor/CodeEditor.js.map +1 -1
  4. package/lib/index.css +2 -2
  5. package/lib/index.css.map +1 -1
  6. package/lib/legend-ai/LegendAIDocEnrichment.d.ts +60 -0
  7. package/lib/legend-ai/LegendAIDocEnrichment.d.ts.map +1 -0
  8. package/lib/legend-ai/LegendAIDocEnrichment.js +429 -0
  9. package/lib/legend-ai/LegendAIDocEnrichment.js.map +1 -0
  10. package/lib/legend-ai/LegendAITypes.d.ts +127 -1
  11. package/lib/legend-ai/LegendAITypes.d.ts.map +1 -1
  12. package/lib/legend-ai/LegendAITypes.js +111 -2
  13. package/lib/legend-ai/LegendAITypes.js.map +1 -1
  14. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts +14 -1
  15. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts.map +1 -1
  16. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js.map +1 -1
  17. package/lib/legend-ai/__test-utils__/LegendAITestUtils.d.ts +2 -1
  18. package/lib/legend-ai/__test-utils__/LegendAITestUtils.d.ts.map +1 -1
  19. package/lib/legend-ai/__test-utils__/LegendAITestUtils.js +37 -2
  20. package/lib/legend-ai/__test-utils__/LegendAITestUtils.js.map +1 -1
  21. package/lib/legend-ai/components/LegendAIAnalysisPanel.d.ts.map +1 -1
  22. package/lib/legend-ai/components/LegendAIAnalysisPanel.js +11 -12
  23. package/lib/legend-ai/components/LegendAIAnalysisPanel.js.map +1 -1
  24. package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts +7 -0
  25. package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts.map +1 -1
  26. package/lib/legend-ai/components/LegendAIAnalysisUtils.js +106 -41
  27. package/lib/legend-ai/components/LegendAIAnalysisUtils.js.map +1 -1
  28. package/lib/legend-ai/components/LegendAIChat.d.ts +1 -5
  29. package/lib/legend-ai/components/LegendAIChat.d.ts.map +1 -1
  30. package/lib/legend-ai/components/LegendAIChat.js +168 -109
  31. package/lib/legend-ai/components/LegendAIChat.js.map +1 -1
  32. package/lib/legend-ai/components/LegendAIChatHelpers.d.ts +21 -0
  33. package/lib/legend-ai/components/LegendAIChatHelpers.d.ts.map +1 -0
  34. package/lib/legend-ai/components/LegendAIChatHelpers.js +85 -0
  35. package/lib/legend-ai/components/LegendAIChatHelpers.js.map +1 -0
  36. package/lib/legend-ai/components/LegendAIChatInput.d.ts +21 -0
  37. package/lib/legend-ai/components/LegendAIChatInput.d.ts.map +1 -0
  38. package/lib/legend-ai/components/LegendAIChatInput.js +78 -0
  39. package/lib/legend-ai/components/LegendAIChatInput.js.map +1 -0
  40. package/lib/legend-ai/components/LegendAIScopeSelector.d.ts +25 -0
  41. package/lib/legend-ai/components/LegendAIScopeSelector.d.ts.map +1 -0
  42. package/lib/legend-ai/components/LegendAIScopeSelector.js +85 -0
  43. package/lib/legend-ai/components/LegendAIScopeSelector.js.map +1 -0
  44. package/lib/legend-ai/index.d.ts +8 -3
  45. package/lib/legend-ai/index.d.ts.map +1 -1
  46. package/lib/legend-ai/index.js +8 -3
  47. package/lib/legend-ai/index.js.map +1 -1
  48. package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts +105 -0
  49. package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts.map +1 -0
  50. package/lib/legend-ai/stores/LegendAIChatProcessors.js +1482 -0
  51. package/lib/legend-ai/stores/LegendAIChatProcessors.js.map +1 -0
  52. package/lib/legend-ai/stores/LegendAIChatState.d.ts +2 -35
  53. package/lib/legend-ai/stores/LegendAIChatState.d.ts.map +1 -1
  54. package/lib/legend-ai/stores/LegendAIChatState.js +114 -949
  55. package/lib/legend-ai/stores/LegendAIChatState.js.map +1 -1
  56. package/package.json +5 -5
  57. package/src/code-editor/CodeEditor.tsx +19 -0
  58. package/src/legend-ai/LegendAIDocEnrichment.ts +572 -0
  59. package/src/legend-ai/LegendAITypes.ts +213 -5
  60. package/src/legend-ai/LegendAI_LegendApplicationPlugin_Extension.ts +25 -0
  61. package/src/legend-ai/__test-utils__/LegendAITestUtils.ts +55 -1
  62. package/src/legend-ai/components/LegendAIAnalysisPanel.tsx +14 -34
  63. package/src/legend-ai/components/LegendAIAnalysisUtils.ts +157 -47
  64. package/src/legend-ai/components/LegendAIChat.tsx +389 -206
  65. package/src/legend-ai/components/LegendAIChatHelpers.ts +117 -0
  66. package/src/legend-ai/components/LegendAIChatInput.tsx +209 -0
  67. package/src/legend-ai/components/LegendAIScopeSelector.tsx +199 -0
  68. package/src/legend-ai/index.ts +31 -4
  69. package/src/legend-ai/stores/LegendAIChatProcessors.ts +2563 -0
  70. package/src/legend-ai/stores/LegendAIChatState.ts +161 -1697
  71. package/tsconfig.json +5 -0
@@ -13,945 +13,34 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import { useState, useRef, useCallback, useEffect } from 'react';
17
- import { assertErrorThrown, noop, uuid } from '@finos/legend-shared';
18
- import { LegendAIQuestionIntent, LegendAIThinkingStepStatus, LegendAIMessageRole, LegendAIErrorType, LegendAIServiceError, TDSServiceSourceType, buildColumnDefsFromNames, LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID, } from '../LegendAITypes.js';
19
- import { LegendAIJudgeVerdict, } from '../LegendAI_LegendApplicationPlugin_Extension.js';
20
- const MAX_ERROR_MESSAGE_LENGTH = 500;
21
- const MAX_THINKING_ERROR_PREVIEW_LENGTH = 200;
22
- const DEFAULT_MAX_JUDGE_ATTEMPTS = 5;
23
- const DEFAULT_MAX_EXECUTION_RETRIES = 3;
24
- const ANALYSIS_TIMEOUT_MS = 15_000;
25
- const SUGGESTED_QUERIES_DELIMITER = '---SUGGESTED_QUERIES---';
26
- export function elapsedSeconds(startTime, decimals = 1) {
27
- return ((Date.now() - startTime) / 1000).toFixed(decimals);
28
- }
29
- function withTimeout(promise, ms) {
30
- let timer;
31
- return Promise.race([
32
- promise.finally(() => {
33
- if (timer !== undefined) {
34
- clearTimeout(timer);
35
- }
36
- }),
37
- new Promise((resolve) => {
38
- timer = setTimeout(() => resolve(undefined), ms);
39
- }),
40
- ]);
41
- }
42
- function deduplicateColumns(columns) {
43
- const seen = new Map();
44
- return columns.map((col) => {
45
- const count = seen.get(col) ?? 0;
46
- seen.set(col, count + 1);
47
- return count === 0 ? col : `${col}_${count + 1}`;
48
- });
49
- }
50
- export function createMessagePair(text) {
51
- return [
52
- { id: uuid(), role: LegendAIMessageRole.USER, text },
53
- {
54
- id: uuid(),
55
- role: LegendAIMessageRole.ASSISTANT,
56
- thinkingSteps: [],
57
- sql: null,
58
- textAnswer: null,
59
- dataContext: null,
60
- gridData: null,
61
- error: null,
62
- errorType: null,
63
- sqlGenTime: null,
64
- execTime: null,
65
- thinkingDuration: null,
66
- isProcessing: true,
67
- isExecuting: false,
68
- suggestedQueries: [],
69
- fallbackAction: null,
70
- },
71
- ];
72
- }
73
- export function updateLastAssistant(setMessages, updater) {
74
- setMessages((prev) => {
75
- const newMsgs = [...prev];
76
- const lastIdx = newMsgs.length - 1;
77
- const last = newMsgs[lastIdx];
78
- if (last?.role === LegendAIMessageRole.ASSISTANT) {
79
- newMsgs[lastIdx] = { ...last, ...updater(last) };
80
- }
81
- return newMsgs;
82
- });
83
- }
84
- export function addThinkingStep(setMessages, label) {
85
- updateLastAssistant(setMessages, (msg) => ({
86
- thinkingSteps: [
87
- ...msg.thinkingSteps.map((s) => s.status === LegendAIThinkingStepStatus.ACTIVE
88
- ? { ...s, status: LegendAIThinkingStepStatus.DONE }
89
- : s),
90
- { id: uuid(), label, status: LegendAIThinkingStepStatus.ACTIVE },
91
- ],
92
- }));
93
- }
94
- export function completeThinkingSteps(setMessages) {
95
- updateLastAssistant(setMessages, (msg) => ({
96
- thinkingSteps: msg.thinkingSteps.map((s) => s.status === LegendAIThinkingStepStatus.ACTIVE
97
- ? { ...s, status: LegendAIThinkingStepStatus.DONE }
98
- : s),
99
- }));
100
- }
101
- export function classifyError(error) {
102
- if (error instanceof LegendAIServiceError) {
103
- return error.errorType;
104
- }
105
- return LegendAIErrorType.GENERAL;
106
- }
107
- export function finishWithThinkingError(setMessages, errorMsg, startTime, errorType) {
108
- updateLastAssistant(setMessages, (msg) => ({
109
- thinkingSteps: msg.thinkingSteps.map((s) => s.status === LegendAIThinkingStepStatus.ACTIVE
110
- ? { ...s, status: LegendAIThinkingStepStatus.ERROR }
111
- : s),
112
- error: errorMsg.slice(0, MAX_ERROR_MESSAGE_LENGTH),
113
- errorType: errorType ?? null,
114
- isProcessing: false,
115
- thinkingDuration: elapsedSeconds(startTime),
116
- }));
117
- }
118
- export function buildConversationHistory(messages) {
119
- const history = [];
120
- let i = 0;
121
- while (i < messages.length - 1) {
122
- const userMsg = messages[i];
123
- const asstMsg = messages[i + 1];
124
- if (userMsg?.role === LegendAIMessageRole.USER &&
125
- asstMsg?.role === LegendAIMessageRole.ASSISTANT) {
126
- if (asstMsg.sql) {
127
- history.push({
128
- question: userMsg.text,
129
- sql: asstMsg.sql,
130
- intent: LegendAIQuestionIntent.DATA_QUERY,
131
- });
132
- }
133
- else if (asstMsg.textAnswer) {
134
- history.push({
135
- question: userMsg.text,
136
- sql: asstMsg.textAnswer,
137
- intent: LegendAIQuestionIntent.METADATA,
138
- });
139
- }
140
- i += 2;
141
- }
142
- else {
143
- i += 1;
144
- }
145
- }
146
- return history;
147
- }
148
- function formatServiceParams(services) {
149
- return services.flatMap((s) => s.parameters.length > 0 ? [`${s.title}: ${s.parameters.join(', ')}`] : []);
150
- }
151
- export function buildGenerationFailureMessage(failure, suggestion, services) {
152
- const parts = [failure];
153
- if (suggestion) {
154
- parts.push(`\nTry instead: "${suggestion}"`);
155
- }
156
- const svcNames = services.map((s) => s.title);
157
- if (svcNames.length > 0) {
158
- parts.push(`\nAvailable services: ${svcNames.join(', ')}`);
159
- }
160
- const allParams = formatServiceParams(services);
161
- if (allParams.length > 0) {
162
- parts.push(`\nService parameters: ${allParams.join('; ')}`);
163
- }
164
- return parts.join('');
165
- }
166
- export function buildExecutionErrorMessage(errStr, services) {
167
- const errParts = [];
168
- const errLower = errStr.toLowerCase();
169
- const missingParamMatch = /missing required parameter values?\s*\[(?<params>[^\]]+)\]/i.exec(errStr);
170
- if (missingParamMatch) {
171
- const paramNames = missingParamMatch.groups?.params ?? '';
172
- const paramList = paramNames.split(',').map((p) => p.trim());
173
- const hint = paramList
174
- .map((p) => `a specific ${p.replaceAll(/(?<lower>[a-z])(?<upper>[A-Z])/g, '$<lower> $<upper>').toLowerCase()}`)
175
- .join(' and ');
176
- errParts.push(`This service requires a value for: ${paramNames}`, `\nTry rephrasing your question to include ${hint}.`);
177
- const svcParams = formatServiceParams(services);
178
- if (svcParams.length > 0) {
179
- errParts.push(`\nService parameters:\n${svcParams.join('\n')}`);
180
- }
181
- return errParts.join('');
182
- }
183
- errParts.push(errStr.slice(0, MAX_ERROR_MESSAGE_LENGTH));
184
- if (errLower.includes('column') &&
185
- (errLower.includes('not found') ||
186
- errLower.includes('does not exist') ||
187
- errLower.includes('unknown'))) {
188
- const svcCols = services.map((s) => `${s.title}: ${s.columns.map((c) => c.name).join(', ')}`);
189
- errParts.push(`\nAvailable columns:\n${svcCols.join('\n')}`);
190
- }
191
- if (errLower.includes('parameter') || errLower.includes('argument')) {
192
- const svcParams = formatServiceParams(services);
193
- if (svcParams.length > 0) {
194
- errParts.push(`\nRequired parameters:\n${svcParams.join('\n')}`);
195
- }
196
- }
197
- return errParts.join('');
198
- }
199
- function parseSuggestedQueries(rawAnswer) {
200
- const delimIndex = rawAnswer.indexOf(SUGGESTED_QUERIES_DELIMITER);
201
- if (delimIndex === -1) {
202
- return { answer: rawAnswer.trim(), suggestedQueries: [] };
203
- }
204
- const answer = rawAnswer.slice(0, delimIndex).trim();
205
- const suggestionsBlock = rawAnswer.slice(delimIndex + SUGGESTED_QUERIES_DELIMITER.length);
206
- const suggestedQueries = suggestionsBlock
207
- .split('\n')
208
- .map((line) => line.replace(/^\d+[.)]\s*/, '').trim())
209
- .filter((line) => line.length > 0)
210
- .slice(0, 3);
211
- return { answer, suggestedQueries };
212
- }
213
- export async function handleMetadataQuestion(question, metadata, context, startTime, hasQueryableServices) {
214
- const { config, plugin, history, setMessages } = context;
215
- addThinkingStep(setMessages, 'Answering from product metadata...');
216
- const metadataPromptText = plugin.buildMetadataPrompt(question, metadata, history);
217
- const rawAnswer = await plugin.callLLM(metadataPromptText, config);
218
- const { answer, suggestedQueries: parsedSuggestions } = parseSuggestedQueries(rawAnswer);
219
- const suggestedQueries = hasQueryableServices === false && !config.orchestratorUrl
220
- ? []
221
- : parsedSuggestions;
222
- completeThinkingSteps(setMessages);
223
- updateLastAssistant(setMessages, () => ({
224
- textAnswer: answer,
225
- suggestedQueries,
226
- isProcessing: false,
227
- thinkingDuration: elapsedSeconds(startTime),
228
- }));
229
- }
230
- export async function generateAndJudgeSql(question, services, coordinates, context, startTime) {
231
- const { config, plugin, history, setMessages } = context;
232
- addThinkingStep(setMessages, 'Building context from service schemas...');
233
- const prompt = plugin.buildGeneratorPrompt(question, services, coordinates, history);
234
- addThinkingStep(setMessages, 'Generating SQL query...');
235
- const answerText = await plugin.callLLM(prompt, config);
236
- const { sql: generatedSql, failure, suggestion, } = plugin.extractSqlFromResponse(answerText);
237
- if (failure) {
238
- addThinkingStep(setMessages, `Generation failed: ${failure}`);
239
- finishWithThinkingError(setMessages, buildGenerationFailureMessage(failure, suggestion, services), startTime, LegendAIErrorType.GENERATION);
240
- return null;
241
- }
242
- if (!generatedSql) {
243
- addThinkingStep(setMessages, 'Could not extract SQL from response');
244
- finishWithThinkingError(setMessages, 'Could not extract SQL from LLM response.\nTry rephrasing your question or ask about a specific service.', startTime, LegendAIErrorType.GENERATION);
245
- return null;
246
- }
247
- const maxAttempts = config.maxJudgeAttempts ?? DEFAULT_MAX_JUDGE_ATTEMPTS;
248
- let currentSql = generatedSql;
249
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
250
- addThinkingStep(setMessages, `Verifying query correctness (${attempt}/${maxAttempts})...`);
251
- const judgePrompt = plugin.buildJudgePrompt(currentSql, question, services, coordinates, history);
252
- const judgeAnswer = await plugin.callLLM(judgePrompt, config);
253
- const judgeResult = plugin.extractJudgeResult(judgeAnswer);
254
- if (judgeResult.verdict === LegendAIJudgeVerdict.PASS) {
255
- completeThinkingSteps(setMessages);
256
- return currentSql;
257
- }
258
- const previousSql = currentSql;
259
- const correctedSql = judgeResult.correctedSql?.trim();
260
- if (correctedSql !== undefined &&
261
- (correctedSql.toLowerCase().startsWith('select') ||
262
- correctedSql.toLowerCase().startsWith('with') ||
263
- correctedSql.toLowerCase().startsWith('('))) {
264
- addThinkingStep(setMessages, `Query corrected (attempt ${attempt})`);
265
- currentSql = correctedSql;
266
- }
267
- if (currentSql === previousSql || attempt === maxAttempts) {
268
- addThinkingStep(setMessages, 'Max verification attempts reached, using best query');
269
- return currentSql;
270
- }
271
- }
272
- return null;
273
- }
274
- function reportExecutionResult(rawResult, setMessages, execStartTime, startTime) {
275
- const columns = deduplicateColumns(rawResult.columns);
276
- const rows = rawResult.rows;
277
- completeThinkingSteps(setMessages);
278
- addThinkingStep(setMessages, `Retrieved ${rows.length} row${rows.length === 1 ? '' : 's'}`);
279
- completeThinkingSteps(setMessages);
280
- updateLastAssistant(setMessages, () => ({
281
- gridData: { columnDefs: buildColumnDefsFromNames(columns), rowData: rows },
282
- execTime: elapsedSeconds(execStartTime, 2),
283
- isProcessing: false,
284
- isExecuting: false,
285
- thinkingDuration: elapsedSeconds(startTime),
286
- }));
287
- return { columns, rows };
288
- }
289
- export async function executeSqlAndReport(sql, services, config, plugin, setMessages, startTime, dataProductCoordinates) {
290
- const execStartTime = Date.now();
291
- try {
292
- const isAccessPoint = services.some((s) => s.sourceType === TDSServiceSourceType.ACCESS_POINT);
293
- const rawResult = isAccessPoint && dataProductCoordinates
294
- ? await plugin.executeLakehouseSql(sql, dataProductCoordinates, config)
295
- : await plugin.executeSql(sql, config);
296
- return reportExecutionResult(rawResult, setMessages, execStartTime, startTime);
297
- }
298
- catch (executeError) {
299
- assertErrorThrown(executeError);
300
- const execErrorType = classifyError(executeError);
301
- addThinkingStep(setMessages, `Execution failed: ${executeError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
302
- finishWithThinkingError(setMessages, buildExecutionErrorMessage(executeError.message, services), startTime, execErrorType === LegendAIErrorType.GENERAL
303
- ? LegendAIErrorType.EXECUTION
304
- : execErrorType);
305
- updateLastAssistant(setMessages, () => ({
306
- execTime: elapsedSeconds(execStartTime, 2),
307
- isExecuting: false,
308
- }));
309
- return undefined;
310
- }
311
- }
312
- export async function executePureQueryAndReport(pureQuery, pureExecutionContext, dataProductCoordinates, config, plugin, setMessages, startTime) {
313
- const execStartTime = Date.now();
314
- try {
315
- addThinkingStep(setMessages, 'Executing Pure query...');
316
- const rawResult = await plugin.executePureQuery(pureQuery, pureExecutionContext, dataProductCoordinates, config);
317
- return reportExecutionResult(rawResult, setMessages, execStartTime, startTime);
318
- }
319
- catch (executeError) {
320
- assertErrorThrown(executeError);
321
- const execErrorType = classifyError(executeError);
322
- addThinkingStep(setMessages, `Execution failed: ${executeError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
323
- completeThinkingSteps(setMessages);
324
- updateLastAssistant(setMessages, () => ({
325
- execTime: elapsedSeconds(execStartTime, 2),
326
- isExecuting: false,
327
- isProcessing: false,
328
- error: `Execution failed: ${executeError.message.slice(0, MAX_ERROR_MESSAGE_LENGTH)}`,
329
- errorType: execErrorType === LegendAIErrorType.GENERAL
330
- ? LegendAIErrorType.EXECUTION
331
- : execErrorType,
332
- thinkingDuration: elapsedSeconds(startTime),
333
- }));
334
- return { columns: [], rows: [] };
335
- }
336
- }
337
- export async function analyzeOrchestratorResults(question, query, execResult, metadata, context, startTime) {
338
- const { config, plugin, setMessages } = context;
339
- addThinkingStep(setMessages, 'Analyzing results...');
340
- updateLastAssistant(setMessages, () => ({
341
- isProcessing: true,
342
- }));
343
- const analysis = await withTimeout(plugin.analyzeQueryResults(question, query, execResult.columns, execResult.rows, metadata, config), ANALYSIS_TIMEOUT_MS);
344
- if (analysis) {
345
- completeThinkingSteps(setMessages);
346
- updateLastAssistant(setMessages, () => ({
347
- textAnswer: analysis.summary,
348
- suggestedQueries: analysis.suggestedQueries,
349
- isProcessing: false,
350
- thinkingDuration: elapsedSeconds(startTime),
351
- }));
352
- }
353
- }
354
- async function handleEmptyOrchestratorResults(question, legendQuery, orchestratorOptions, metadata, resolvedEntities, context, startTime) {
355
- const { dataProductCoordinates, pureExecutionContext } = orchestratorOptions;
356
- const { config, plugin, setMessages } = context;
357
- if (resolvedEntities.relatedEntities.length > 0) {
358
- const alternateRoot = resolvedEntities.relatedEntities[0];
359
- if (alternateRoot) {
360
- addThinkingStep(setMessages, `No results with ${resolvedEntities.rootEntity.split('::').pop() ?? resolvedEntities.rootEntity}, retrying with ${alternateRoot.split('::').pop() ?? alternateRoot}...`);
361
- try {
362
- const retryResponse = await plugin.generateQueryViaOrchestrator({
363
- user_question: question,
364
- semantic_search_resolution_details: {
365
- data_product_coordinates: dataProductCoordinates,
366
- root_entity: alternateRoot,
367
- related_entities: resolvedEntities.relatedEntities.slice(1),
368
- },
369
- }, config);
370
- updateLastAssistant(setMessages, () => ({
371
- sql: retryResponse.legend_query,
372
- sqlGenTime: elapsedSeconds(startTime, 2),
373
- isExecuting: true,
374
- }));
375
- const retryResult = await executePureQueryAndReport(retryResponse.legend_query, pureExecutionContext, dataProductCoordinates, config, plugin, setMessages, startTime);
376
- if (retryResult.rows.length > 0) {
377
- await analyzeOrchestratorResults(question, retryResponse.legend_query, retryResult, metadata, context, startTime);
378
- return;
379
- }
380
- }
381
- catch {
382
- /* empty */
383
- }
384
- }
385
- }
386
- addThinkingStep(setMessages, 'No results returned \u2014 building contextual guidance...');
387
- updateLastAssistant(setMessages, () => ({
388
- isProcessing: true,
389
- }));
390
- const fallback = await withTimeout(plugin.buildNoResultsFallback(question, legendQuery, metadata, config), ANALYSIS_TIMEOUT_MS);
391
- if (fallback) {
392
- completeThinkingSteps(setMessages);
393
- updateLastAssistant(setMessages, () => ({
394
- textAnswer: fallback.summary,
395
- suggestedQueries: fallback.suggestedQueries,
396
- isProcessing: false,
397
- thinkingDuration: elapsedSeconds(startTime),
398
- }));
399
- }
400
- }
401
- export async function processQuestionViaOrchestrator(question, dataProductCoordinates, metadata, context, pureExecutionContext, preResolvedEntities) {
402
- const { config, plugin, setMessages } = context;
403
- const startTime = Date.now();
404
- try {
405
- let resolvedEntities;
406
- if (preResolvedEntities) {
407
- resolvedEntities = preResolvedEntities;
408
- addThinkingStep(setMessages, `Using pre-resolved root entity: ${resolvedEntities.rootEntity.split('::').pop() ?? resolvedEntities.rootEntity}`);
409
- }
410
- else {
411
- addThinkingStep(setMessages, 'Resolving entities for your query...');
412
- resolvedEntities = await plugin.resolveEntitiesForQuery(question, dataProductCoordinates, config, pureExecutionContext);
413
- addThinkingStep(setMessages, `Found root entity: ${resolvedEntities.rootEntity.split('::').pop() ?? resolvedEntities.rootEntity}`);
414
- }
415
- if (resolvedEntities.relatedEntities.length > 0) {
416
- addThinkingStep(setMessages, `Found ${resolvedEntities.relatedEntities.length} related entities`);
417
- }
418
- addThinkingStep(setMessages, 'Generating Legend query via orchestrator...');
419
- const orchestratorResponse = await plugin.generateQueryViaOrchestrator({
420
- user_question: question,
421
- semantic_search_resolution_details: {
422
- data_product_coordinates: dataProductCoordinates,
423
- root_entity: resolvedEntities.rootEntity,
424
- related_entities: resolvedEntities.relatedEntities,
425
- },
426
- }, config);
427
- const queryGenTime = elapsedSeconds(startTime, 2);
428
- completeThinkingSteps(setMessages);
429
- updateLastAssistant(setMessages, () => ({
430
- sql: orchestratorResponse.legend_query,
431
- sqlGenTime: queryGenTime,
432
- isExecuting: true,
433
- isProcessing: true,
434
- }));
435
- if (!pureExecutionContext) {
436
- updateLastAssistant(setMessages, () => ({
437
- isProcessing: false,
438
- isExecuting: false,
439
- error: 'No execution context available — cannot execute query via engine.',
440
- errorType: LegendAIErrorType.EXECUTION,
441
- thinkingDuration: elapsedSeconds(startTime),
442
- }));
443
- return;
444
- }
445
- const execResult = await executePureQueryAndReport(orchestratorResponse.legend_query, pureExecutionContext, dataProductCoordinates, config, plugin, setMessages, startTime);
446
- try {
447
- if (execResult.rows.length > 0) {
448
- await analyzeOrchestratorResults(question, orchestratorResponse.legend_query, execResult, metadata, context, startTime);
449
- }
450
- else {
451
- await handleEmptyOrchestratorResults(question, orchestratorResponse.legend_query, { dataProductCoordinates, pureExecutionContext }, metadata, resolvedEntities, context, startTime);
452
- }
453
- }
454
- catch {
455
- /* empty */
456
- }
457
- finally {
458
- completeThinkingSteps(setMessages);
459
- updateLastAssistant(setMessages, () => ({
460
- isProcessing: false,
461
- thinkingDuration: elapsedSeconds(startTime),
462
- }));
463
- }
464
- }
465
- catch (error) {
466
- assertErrorThrown(error);
467
- const orchErrorType = classifyError(error);
468
- addThinkingStep(setMessages, `Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
469
- try {
470
- addThinkingStep(setMessages, 'Building guidance from available metadata...');
471
- const fallbackText = await withTimeout(plugin.buildFailureFallback(question, error.message, metadata, config), ANALYSIS_TIMEOUT_MS);
472
- if (fallbackText) {
473
- completeThinkingSteps(setMessages);
474
- updateLastAssistant(setMessages, () => ({
475
- dataContext: fallbackText,
476
- isProcessing: false,
477
- thinkingDuration: elapsedSeconds(startTime),
478
- }));
479
- return;
480
- }
481
- }
482
- catch {
483
- /* empty */
484
- }
485
- finishWithThinkingError(setMessages, error.message, startTime, orchErrorType);
486
- }
487
- }
488
- function cleanLlmSqlResponse(raw) {
489
- return raw
490
- .trim()
491
- .replace(/^```\w*\n?/, '')
492
- .replace(/\n?```$/, '')
493
- .replace(/;\s*$/, '')
494
- .trim();
495
- }
496
- function isValidSqlCorrection(trimmed, currentSql) {
497
- return (trimmed.length > 0 &&
498
- trimmed.toLowerCase().startsWith('select') &&
499
- trimmed !== currentSql);
500
- }
501
- const ALIAS_DOT_COL_PATTERN = /\b(?<tbl>[a-z]\w*)\s*\.\s*"(?<col>[^"]+)"/gi;
502
- const JOIN_PATTERN = /\bJOIN\b/i;
503
- const ORDER_BY_SPLIT = /\bORDER\s+BY\b/i;
504
- export function sanitizeJoinOrderBy(sql) {
505
- if (!JOIN_PATTERN.test(sql)) {
506
- return sql;
507
- }
508
- const parts = sql.split(ORDER_BY_SPLIT);
509
- if (parts.length < 2) {
510
- return sql;
511
- }
512
- const beforeOrderBy = parts[0] ?? '';
513
- const afterOrderBy = parts.slice(1).join('ORDER BY').replace(/^\s+/, '');
514
- const selectAliases = new Map();
515
- const aliasRegex = /\b(?<tbl>[a-z]\w*)\s*\.\s*"(?<col>[^"]+)"\s+AS\s+(?:"(?<qAlias>[^"]+)"|(?<uAlias>\w+))/gi;
516
- let m;
517
- while ((m = aliasRegex.exec(beforeOrderBy)) !== null) {
518
- const tableAlias = (m.groups?.tbl ?? '').toLowerCase();
519
- const colName = (m.groups?.col ?? '').toLowerCase();
520
- const asAlias = m.groups?.qAlias ?? m.groups?.uAlias ?? '';
521
- selectAliases.set(`${tableAlias}.${colName}`, asAlias);
522
- }
523
- if (selectAliases.size === 0) {
524
- return sql;
525
- }
526
- const rewritten = afterOrderBy.replaceAll(ALIAS_DOT_COL_PATTERN, (...args) => {
527
- const groups = args[args.length - 1];
528
- const key = `${groups.tbl.toLowerCase()}.${groups.col.toLowerCase()}`;
529
- const alias = selectAliases.get(key);
530
- return alias ? `"${alias}"` : String(args[0]);
531
- });
532
- if (rewritten === afterOrderBy) {
533
- return sql;
534
- }
535
- return `${beforeOrderBy}ORDER BY ${rewritten}`;
536
- }
537
- const UNION_ALL_PATTERN = /\bUNION\s+ALL\b/i;
538
- const LITERAL_COL_PATTERN = /,\s*'[^']*'\s+AS\s+(?:"[^"]+"|[a-z]\w*)/gi;
539
- export function sanitizeLiteralColumns(sql) {
540
- if (!UNION_ALL_PATTERN.test(sql)) {
541
- return sql;
542
- }
543
- LITERAL_COL_PATTERN.lastIndex = 0;
544
- if (!LITERAL_COL_PATTERN.test(sql)) {
545
- return sql;
546
- }
547
- LITERAL_COL_PATTERN.lastIndex = 0;
548
- return sql.replace(LITERAL_COL_PATTERN, '');
549
- }
550
- const SERVICE_PARAM_DATE_LIKE = /date|time|day|month|year|period|asOf|businessDate|processingDate|snapshot/i;
551
- function hasUnresolvableParams(service) {
552
- return service.parameters.some((p) => !SERVICE_PARAM_DATE_LIKE.test(p));
553
- }
554
- function getNonDateParamNames(service) {
555
- return service.parameters.filter((p) => !SERVICE_PARAM_DATE_LIKE.test(p));
556
- }
557
- export function stripNonDateServiceParams(sql) {
558
- return sql.replaceAll(/,\s*\w+\s*=>\s*'[^']*'/g, (match) => {
559
- const paramName = /,\s*(?<param>\w+)\s*=>/.exec(match)?.groups?.param;
560
- if (!paramName) {
561
- return match;
562
- }
563
- if (paramName === 'coordinates' ||
564
- SERVICE_PARAM_DATE_LIKE.test(paramName)) {
565
- return match;
566
- }
567
- return '';
568
- });
569
- }
570
- async function executeSqlForServices(sql, services, dataProductCoordinates, plugin, config) {
571
- const safeSql = sanitizeLiteralColumns(sanitizeJoinOrderBy(sql));
572
- const isAccessPoint = services.some((s) => s.sourceType === TDSServiceSourceType.ACCESS_POINT);
573
- if (isAccessPoint && dataProductCoordinates) {
574
- return plugin.executeLakehouseSql(safeSql, dataProductCoordinates, config);
575
- }
576
- return plugin.executeSql(safeSql, config);
577
- }
578
- async function executeSqlWithRetries(initialSql, question, services, coordinates, dataProductCoordinates, context) {
579
- const { plugin, config, setMessages } = context;
580
- let currentSql = initialSql;
581
- for (let attempt = 0; attempt <= DEFAULT_MAX_EXECUTION_RETRIES; attempt++) {
582
- try {
583
- const result = await executeSqlForServices(currentSql, services, dataProductCoordinates, plugin, config);
584
- return { sql: currentSql, result };
585
- }
586
- catch (executeError) {
587
- assertErrorThrown(executeError);
588
- if (attempt >= DEFAULT_MAX_EXECUTION_RETRIES) {
589
- return { sql: currentSql, error: executeError.message };
590
- }
591
- addThinkingStep(setMessages, `Execution failed (attempt ${attempt + 1}/${DEFAULT_MAX_EXECUTION_RETRIES + 1}), correcting query...`);
592
- const corrected = await attemptErrorCorrection(currentSql, executeError.message, question, services, coordinates, plugin, config);
593
- if (corrected) {
594
- currentSql = corrected;
595
- updateLastAssistant(setMessages, () => ({ sql: currentSql }));
596
- continue;
597
- }
598
- return { sql: currentSql, error: executeError.message };
599
- }
600
- }
601
- return { sql: currentSql };
602
- }
603
- async function attemptErrorCorrection(currentSql, errorMessage, question, services, coordinates, plugin, config) {
604
- const prompt = plugin.buildErrorCorrectionPrompt(currentSql, errorMessage, question, services, coordinates);
605
- if (!prompt) {
606
- return undefined;
607
- }
608
- try {
609
- const correctedSql = await plugin.callLLM(prompt, config);
610
- const trimmed = cleanLlmSqlResponse(correctedSql);
611
- if (isValidSqlCorrection(trimmed, currentSql)) {
612
- return trimmed;
613
- }
614
- }
615
- catch {
616
- /* empty */
617
- }
618
- return undefined;
619
- }
620
- async function attemptZeroRowCorrection(currentSql, question, services, coordinates, dataProductCoordinates, context) {
621
- const { plugin, config, setMessages } = context;
622
- addThinkingStep(setMessages, 'Query returned 0 rows, attempting filter correction...');
623
- const prompt = plugin.buildZeroRowCorrectionPrompt(currentSql, question, services, coordinates);
624
- if (!prompt) {
625
- return undefined;
626
- }
627
- try {
628
- const correctedSql = await plugin.callLLM(prompt, config);
629
- const trimmed = cleanLlmSqlResponse(correctedSql);
630
- if (!isValidSqlCorrection(trimmed, currentSql)) {
631
- return undefined;
632
- }
633
- addThinkingStep(setMessages, 'Retrying with corrected filters...');
634
- updateLastAssistant(setMessages, () => ({ sql: trimmed }));
635
- try {
636
- const retryResult = await executeSqlForServices(trimmed, services, dataProductCoordinates, plugin, config);
637
- if (retryResult.rows.length > 0) {
638
- return { sql: trimmed, result: retryResult };
639
- }
640
- }
641
- catch {
642
- /* empty */
643
- }
644
- }
645
- catch {
646
- /* empty */
647
- }
648
- return undefined;
649
- }
650
- function buildZeroRowMessage(services) {
651
- const withUnresolvable = services.filter((s) => hasUnresolvableParams(s));
652
- if (withUnresolvable.length > 0) {
653
- const parts = [];
654
- for (const svc of withUnresolvable) {
655
- for (const paramName of getNonDateParamNames(svc)) {
656
- const matchingCol = svc.columns.find((c) => c.name === paramName);
657
- const docHint = matchingCol?.documentation ?? matchingCol?.sampleValues;
658
- if (docHint) {
659
- parts.push(`**${paramName}** (${docHint})`);
660
- }
661
- else {
662
- parts.push(`**${paramName}**`);
663
- }
664
- }
665
- }
666
- const uniqueParts = [...new Set(parts)];
667
- const firstSvc = withUnresolvable[0];
668
- const firstParam = firstSvc
669
- ? (getNonDateParamNames(firstSvc)[0] ?? 'parameter')
670
- : 'parameter';
671
- return `The SQL query executed successfully but returned **0 rows**. This service requires specific values for ${uniqueParts.join(', ')} to return data. Please include ${uniqueParts.length === 1 ? 'a value' : 'values'} in your question, e.g., "show data where ${firstParam} is [your value]".`;
672
- }
673
- return 'The SQL query executed successfully but returned **0 rows**. The applied filters may not match any records, or the specific values may not exist in the queried datasets.';
674
- }
675
- function offerOrchestratorFallbackMessage(setMessages, startTime, fallbackMessage) {
676
- updateLastAssistant(setMessages, () => ({
677
- textAnswer: fallbackMessage,
678
- fallbackAction: {
679
- label: 'Try Legend AI Orchestrator',
680
- actionId: LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
681
- },
682
- isProcessing: false,
683
- thinkingDuration: elapsedSeconds(startTime),
684
- }));
685
- }
686
- function reportFatalQueryError(setMessages, startTime, errorMessage, errorType) {
687
- finishWithThinkingError(setMessages, errorMessage, startTime, errorType);
688
- }
689
- function handleSqlGenerationFailure(setMessages, startTime, hasOrchestratorFallback, orchestratorMessage, errorMessage, errorType) {
690
- completeThinkingSteps(setMessages);
691
- if (hasOrchestratorFallback) {
692
- offerOrchestratorFallbackMessage(setMessages, startTime, orchestratorMessage);
693
- }
694
- else {
695
- reportFatalQueryError(setMessages, startTime, errorMessage, errorType);
696
- }
697
- }
698
- async function reportQueryResults(report, metadata, context, startTime, hasOrchestratorFallback) {
699
- const { currentSql, sqlResult, question, services } = report;
700
- const { setMessages } = context;
701
- if (sqlResult.rows.length > 0) {
702
- const columns = deduplicateColumns(sqlResult.columns);
703
- const rows = sqlResult.rows;
704
- completeThinkingSteps(setMessages);
705
- addThinkingStep(setMessages, `Retrieved ${rows.length} row${rows.length === 1 ? '' : 's'}`);
706
- completeThinkingSteps(setMessages);
707
- updateLastAssistant(setMessages, () => ({
708
- sql: currentSql,
709
- gridData: {
710
- columnDefs: buildColumnDefsFromNames(columns),
711
- rowData: rows,
712
- },
713
- execTime: elapsedSeconds(startTime, 2),
714
- isProcessing: true,
715
- isExecuting: false,
716
- thinkingDuration: elapsedSeconds(startTime),
717
- }));
718
- try {
719
- await analyzeOrchestratorResults(question, currentSql, sqlResult, metadata, context, startTime);
720
- }
721
- catch {
722
- /* empty */
723
- }
724
- finally {
725
- completeThinkingSteps(setMessages);
726
- updateLastAssistant(setMessages, () => ({
727
- isProcessing: false,
728
- thinkingDuration: elapsedSeconds(startTime),
729
- }));
730
- }
731
- }
732
- else {
733
- addThinkingStep(setMessages, 'Query returned 0 rows after correction attempts.');
734
- completeThinkingSteps(setMessages);
735
- const fallback = hasOrchestratorFallback
736
- ? {
737
- fallbackAction: {
738
- label: 'Try Legend AI Orchestrator',
739
- actionId: LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
740
- },
741
- }
742
- : {};
743
- updateLastAssistant(setMessages, () => ({
744
- textAnswer: buildZeroRowMessage(services),
745
- ...fallback,
746
- isProcessing: false,
747
- isExecuting: false,
748
- thinkingDuration: elapsedSeconds(startTime),
749
- }));
750
- }
751
- }
752
- async function selectBestServices(question, services, context) {
753
- const { plugin, config, setMessages } = context;
754
- if (services.length <= 1) {
755
- return services;
756
- }
757
- try {
758
- addThinkingStep(setMessages, 'Selecting best service for your query...');
759
- return await plugin.selectRelevantServices(question, services, config);
760
- }
761
- catch {
762
- return services;
763
- }
764
- }
765
- async function tryRecoverZeroRows(currentSql, sqlResult, question, selectedServices, coordinates, dataProductCoordinates, context) {
766
- const { plugin, config, setMessages } = context;
767
- let recoveredSql = currentSql;
768
- let recoveredResult = sqlResult;
769
- const strippedSql = stripNonDateServiceParams(recoveredSql);
770
- if (strippedSql !== recoveredSql) {
771
- addThinkingStep(setMessages, 'Trying query without guessed parameter values...');
772
- try {
773
- const strippedResult = await executeSqlForServices(strippedSql, selectedServices, dataProductCoordinates, plugin, config);
774
- if (strippedResult.rows.length > 0) {
775
- recoveredSql = strippedSql;
776
- recoveredResult = strippedResult;
777
- updateLastAssistant(setMessages, () => ({ sql: strippedSql }));
778
- }
779
- }
780
- catch {
781
- /* empty */
782
- }
783
- }
784
- if (recoveredResult.rows.length === 0) {
785
- const correction = await attemptZeroRowCorrection(recoveredSql, question, selectedServices, coordinates, dataProductCoordinates, context);
786
- if (correction) {
787
- recoveredSql = correction.sql;
788
- recoveredResult = correction.result;
789
- }
790
- }
791
- return { sql: recoveredSql, result: recoveredResult };
792
- }
793
- async function processDataQuery(question, services, coordinates, metadata, context, startTime, orchestratorOptions) {
794
- const { config, setMessages } = context;
795
- const dataProductCoordinates = orchestratorOptions?.dataProductCoordinates;
796
- const hasOrchestratorFallback = Boolean(config.orchestratorUrl && dataProductCoordinates);
797
- if (services.length === 0) {
798
- handleSqlGenerationFailure(setMessages, startTime, hasOrchestratorFallback, 'No TDS services available for SQL querying. You can try the Legend AI Orchestrator to generate a Pure query instead.', 'No TDS services available for querying', LegendAIErrorType.GENERAL);
799
- return;
800
- }
801
- addThinkingStep(setMessages, 'Found relevant services to query');
802
- const selectedServices = await selectBestServices(question, services, context);
803
- const judgedSql = await generateAndJudgeSql(question, selectedServices, coordinates, context, startTime);
804
- if (!judgedSql) {
805
- addThinkingStep(setMessages, 'SQL generation could not produce a valid query.');
806
- handleSqlGenerationFailure(setMessages, startTime, hasOrchestratorFallback, 'SQL generation could not handle this query. You can try the Legend AI Orchestrator to generate a Pure query instead.', 'SQL generation could not handle this query. Try rephrasing your question.', LegendAIErrorType.GENERATION);
807
- return;
808
- }
809
- const sqlGenTimeValue = elapsedSeconds(startTime, 2);
810
- completeThinkingSteps(setMessages);
811
- updateLastAssistant(setMessages, () => ({
812
- sql: judgedSql,
813
- sqlGenTime: sqlGenTimeValue,
814
- isExecuting: true,
815
- }));
816
- const execOutcome = await executeSqlWithRetries(judgedSql, question, selectedServices, coordinates, dataProductCoordinates, context);
817
- if (execOutcome.error) {
818
- const execErrorType = classifyError(new Error(execOutcome.error));
819
- addThinkingStep(setMessages, `Execution failed: ${execOutcome.error.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
820
- finishWithThinkingError(setMessages, buildExecutionErrorMessage(execOutcome.error, selectedServices), startTime, execErrorType === LegendAIErrorType.GENERAL
821
- ? LegendAIErrorType.EXECUTION
822
- : execErrorType);
823
- updateLastAssistant(setMessages, () => ({
824
- isExecuting: false,
825
- ...(hasOrchestratorFallback
826
- ? {
827
- fallbackAction: {
828
- label: 'Try Legend AI Orchestrator',
829
- actionId: LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
830
- },
831
- }
832
- : {}),
833
- }));
834
- return;
835
- }
836
- if (!execOutcome.result) {
837
- return;
838
- }
839
- let currentSql = execOutcome.sql;
840
- let sqlResult = execOutcome.result;
841
- if (sqlResult.rows.length === 0) {
842
- const recovered = await tryRecoverZeroRows(currentSql, sqlResult, question, selectedServices, coordinates, dataProductCoordinates, context);
843
- currentSql = recovered.sql;
844
- sqlResult = recovered.result;
845
- }
846
- await reportQueryResults({
847
- currentSql,
848
- sqlResult,
849
- question,
850
- services: selectedServices,
851
- }, metadata, context, startTime, hasOrchestratorFallback);
852
- }
853
- export async function processQuestion(question, services, coordinates, metadata, context, dataProductCoordinates, pureExecutionContext) {
854
- const { config, plugin, setMessages } = context;
855
- const startTime = Date.now();
856
- try {
857
- addThinkingStep(setMessages, 'Analyzing your question...');
858
- const orchestratorOpts = dataProductCoordinates
859
- ? {
860
- dataProductCoordinates,
861
- ...(pureExecutionContext === undefined
862
- ? {}
863
- : { pureExecutionContext }),
864
- }
865
- : undefined;
866
- if (services.length > 0) {
867
- const serviceNames = services.map((s) => s.title);
868
- const intent = await plugin.classifyQuestionIntent(question, true, config, serviceNames);
869
- if (intent === LegendAIQuestionIntent.METADATA) {
870
- await handleMetadataQuestion(question, metadata, context, startTime, true);
871
- return;
872
- }
873
- // DATA_QUERY or ORCHESTRATOR — try SQL generation.
874
- // If SQL throws, fall back to metadata as a safety net
875
- // (e.g. misclassified metadata question).
876
- try {
877
- await processDataQuery(question, services, coordinates, metadata, context, startTime, orchestratorOpts);
878
- }
879
- catch (sqlError) {
880
- assertErrorThrown(sqlError);
881
- addThinkingStep(setMessages, 'SQL generation failed, answering from product metadata...');
882
- await handleMetadataQuestion(question, metadata, context, startTime, true);
883
- }
884
- return;
885
- }
886
- // No services available — use orchestrator if configured, else metadata only.
887
- if (config.orchestratorUrl && dataProductCoordinates) {
888
- completeThinkingSteps(setMessages);
889
- await processQuestionViaOrchestrator(question, dataProductCoordinates, metadata, context, pureExecutionContext);
890
- }
891
- else {
892
- await handleMetadataQuestion(question, metadata, context, startTime, false);
893
- }
894
- }
895
- catch (error) {
896
- assertErrorThrown(error);
897
- addThinkingStep(setMessages, `Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
898
- finishWithThinkingError(setMessages, error.message, startTime, classifyError(error));
899
- }
900
- }
901
- export async function processQuestionWithIntent(question, intent, services, coordinates, metadata, context, orchestratorOptions) {
902
- const { config, setMessages } = context;
903
- const dataProductCoordinates = orchestratorOptions?.dataProductCoordinates;
904
- const pureExecutionContext = orchestratorOptions?.pureExecutionContext;
905
- if (intent === LegendAIQuestionIntent.METADATA) {
906
- const startTime = Date.now();
907
- try {
908
- await handleMetadataQuestion(question, metadata, context, startTime, services.length > 0);
909
- }
910
- catch (error) {
911
- assertErrorThrown(error);
912
- addThinkingStep(setMessages, `Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
913
- finishWithThinkingError(setMessages, error.message, startTime, classifyError(error));
914
- }
915
- return;
916
- }
917
- if (intent === LegendAIQuestionIntent.ORCHESTRATOR) {
918
- if (config.orchestratorUrl && dataProductCoordinates) {
919
- // When services are available, try SQL first even for ORCHESTRATOR intent
920
- if (services.length > 0) {
921
- const startTime = Date.now();
922
- try {
923
- addThinkingStep(setMessages, 'Preparing data query...');
924
- await processDataQuery(question, services, coordinates, metadata, context, startTime, orchestratorOptions);
925
- }
926
- catch (error) {
927
- assertErrorThrown(error);
928
- addThinkingStep(setMessages, `Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
929
- finishWithThinkingError(setMessages, error.message, startTime, classifyError(error));
930
- }
931
- return;
932
- }
933
- await processQuestionViaOrchestrator(question, dataProductCoordinates, metadata, context, pureExecutionContext);
934
- return;
935
- }
936
- }
937
- const startTime = Date.now();
938
- try {
939
- addThinkingStep(setMessages, 'Preparing data query...');
940
- await processDataQuery(question, services, coordinates, metadata, context, startTime, orchestratorOptions);
941
- }
942
- catch (error) {
943
- assertErrorThrown(error);
944
- addThinkingStep(setMessages, `Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
945
- finishWithThinkingError(setMessages, error.message, startTime, classifyError(error));
946
- }
947
- }
16
+ import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
17
+ import { noop } from '@finos/legend-shared';
18
+ import { LegendAIMessageRole, } from '../LegendAITypes.js';
19
+ import {} from '../LegendAI_LegendApplicationPlugin_Extension.js';
20
+ import { buildConversationHistory, createMessagePair, processQuestion, processQuestionWithIntent, processQuestionViaOrchestrator, } from './LegendAIChatProcessors.js';
948
21
  export const useLegendAIChatState = (services, coordinates, config, metadata, plugin, dataProductCoordinates, pureExecutionContext) => {
22
+ const LEGEND_AI_MCP_SCOPE_ID = 'legend-ai-mcp';
949
23
  const [questionText, setQuestionText] = useState('');
950
24
  const [isSending, setIsSending] = useState(false);
951
25
  const [messages, setMessages] = useState([]);
952
26
  const [expandedThinking, setExpandedThinking] = useState(new Set());
27
+ const [selectedScopes, setSelectedScopes] = useState([]);
28
+ const [selectedModelName, setSelectedModelName] = useState(undefined);
29
+ const availableModelNames = useMemo(() => {
30
+ const names = new Set();
31
+ if (config.llmModelName) {
32
+ names.add(config.llmModelName);
33
+ }
34
+ for (const modelName of config.llmModelOptions ?? []) {
35
+ if (modelName.trim()) {
36
+ names.add(modelName.trim());
37
+ }
38
+ }
39
+ return Array.from(names);
40
+ }, [config.llmModelName, config.llmModelOptions]);
953
41
  const conversationRef = useRef(null);
954
42
  const sendTimeoutRef = useRef(undefined);
43
+ const cancelledRef = useRef(false);
955
44
  useEffect(() => {
956
45
  const el = conversationRef.current;
957
46
  if (el && messages.length > 0) {
@@ -977,6 +66,38 @@ export const useLegendAIChatState = (services, coordinates, config, metadata, pl
977
66
  return next;
978
67
  });
979
68
  }, []);
69
+ const toggleScope = useCallback((scope) => {
70
+ setSelectedScopes((prev) => prev.some((s) => s.id === scope.id)
71
+ ? prev.filter((s) => s.id !== scope.id)
72
+ : [...prev, scope]);
73
+ }, []);
74
+ const removeScope = useCallback((scopeId) => {
75
+ setSelectedScopes((prev) => prev.filter((s) => s.id !== scopeId));
76
+ }, []);
77
+ const configForRequest = useMemo(() => ({
78
+ ...config,
79
+ llmModelName: selectedModelName ?? config.llmModelName,
80
+ }), [config, selectedModelName]);
81
+ const stopGeneration = useCallback(() => {
82
+ cancelledRef.current = true;
83
+ if (sendTimeoutRef.current !== undefined) {
84
+ clearTimeout(sendTimeoutRef.current);
85
+ sendTimeoutRef.current = undefined;
86
+ }
87
+ setIsSending(false);
88
+ setMessages((prev) => {
89
+ const updated = [...prev];
90
+ const last = updated[updated.length - 1];
91
+ if (last?.role === LegendAIMessageRole.ASSISTANT) {
92
+ const stopped = { ...last, isProcessing: false, isExecuting: false };
93
+ if (!stopped.textAnswer && !stopped.sql && !stopped.error) {
94
+ stopped.textAnswer = 'Generation stopped.';
95
+ }
96
+ updated[updated.length - 1] = stopped;
97
+ }
98
+ return updated;
99
+ });
100
+ }, []);
980
101
  const clearChat = useCallback(() => {
981
102
  setMessages([]);
982
103
  setExpandedThinking(new Set());
@@ -994,6 +115,7 @@ export const useLegendAIChatState = (services, coordinates, config, metadata, pl
994
115
  }
995
116
  const history = buildConversationHistory(messages);
996
117
  setIsSending(true);
118
+ cancelledRef.current = false;
997
119
  setQuestionText('');
998
120
  setMessages((prev) => [...prev, ...createMessagePair(trimmed)]);
999
121
  if (sendTimeoutRef.current !== undefined) {
@@ -1002,46 +124,77 @@ export const useLegendAIChatState = (services, coordinates, config, metadata, pl
1002
124
  }
1003
125
  sendTimeoutRef.current = setTimeout(() => {
1004
126
  process(trimmed, history)
1005
- .catch(noop())
127
+ .catch(noop)
1006
128
  .finally(() => {
1007
- setIsSending(false);
129
+ if (!cancelledRef.current) {
130
+ setIsSending(false);
131
+ }
1008
132
  sendTimeoutRef.current = undefined;
1009
133
  });
1010
134
  }, 0);
1011
135
  }, [isSending, messages]);
1012
- const askQuestion = useCallback(() => dispatchQuestion(questionText, (trimmed, history) => processQuestion(trimmed, services, coordinates, metadata, { config, plugin, history, setMessages }, dataProductCoordinates, pureExecutionContext)), [
136
+ const askQuestion = useCallback(() => dispatchQuestion(questionText, (trimmed, history) => selectedScopes.some((scope) => scope.id === LEGEND_AI_MCP_SCOPE_ID) &&
137
+ configForRequest.orchestratorUrl &&
138
+ dataProductCoordinates
139
+ ? processQuestionViaOrchestrator(trimmed, dataProductCoordinates, metadata, {
140
+ config: configForRequest,
141
+ plugin,
142
+ history,
143
+ setMessages,
144
+ }, pureExecutionContext)
145
+ : processQuestion(trimmed, services, coordinates, metadata, {
146
+ config: configForRequest,
147
+ plugin,
148
+ history,
149
+ setMessages,
150
+ }, dataProductCoordinates, pureExecutionContext)), [
1013
151
  questionText,
1014
152
  dispatchQuestion,
1015
153
  services,
1016
154
  coordinates,
1017
- config,
155
+ configForRequest,
1018
156
  metadata,
1019
157
  plugin,
1020
158
  dataProductCoordinates,
1021
159
  pureExecutionContext,
160
+ selectedScopes,
1022
161
  ]);
1023
- const askQuestionWithIntent = useCallback((text, intent) => dispatchQuestion(text, (trimmed, history) => processQuestionWithIntent(trimmed, intent, services, coordinates, metadata, { config, plugin, history, setMessages }, dataProductCoordinates
1024
- ? {
1025
- dataProductCoordinates,
1026
- ...(pureExecutionContext === undefined
1027
- ? {}
1028
- : { pureExecutionContext }),
1029
- }
1030
- : undefined)), [
162
+ const askQuestionWithIntent = useCallback((text, intent) => dispatchQuestion(text, (trimmed, history) => selectedScopes.some((scope) => scope.id === LEGEND_AI_MCP_SCOPE_ID) &&
163
+ configForRequest.orchestratorUrl &&
164
+ dataProductCoordinates
165
+ ? processQuestionViaOrchestrator(trimmed, dataProductCoordinates, metadata, {
166
+ config: configForRequest,
167
+ plugin,
168
+ history,
169
+ setMessages,
170
+ }, pureExecutionContext)
171
+ : processQuestionWithIntent(trimmed, intent, services, coordinates, metadata, {
172
+ config: configForRequest,
173
+ plugin,
174
+ history,
175
+ setMessages,
176
+ }, dataProductCoordinates
177
+ ? {
178
+ dataProductCoordinates,
179
+ ...(pureExecutionContext === undefined
180
+ ? {}
181
+ : { pureExecutionContext }),
182
+ }
183
+ : undefined)), [
1031
184
  dispatchQuestion,
1032
185
  services,
1033
186
  coordinates,
1034
- config,
187
+ configForRequest,
1035
188
  metadata,
1036
189
  plugin,
1037
190
  dataProductCoordinates,
1038
191
  pureExecutionContext,
192
+ selectedScopes,
1039
193
  ]);
1040
194
  const runFallbackAction = useCallback((messageId) => {
1041
195
  if (isSending || !config.orchestratorUrl || !dataProductCoordinates) {
1042
196
  return;
1043
197
  }
1044
- // Find the user question associated with this assistant message
1045
198
  let question;
1046
199
  for (let i = 0; i < messages.length; i++) {
1047
200
  const msg = messages[i];
@@ -1062,9 +215,13 @@ export const useLegendAIChatState = (services, coordinates, config, metadata, pl
1062
215
  ? { ...m, fallbackAction: null, error: null, isProcessing: true }
1063
216
  : m));
1064
217
  const history = buildConversationHistory(messages);
1065
- const q = question;
1066
- processQuestionViaOrchestrator(q, dataProductCoordinates, metadata, { config, plugin, history, setMessages }, pureExecutionContext)
1067
- .catch(noop())
218
+ processQuestionViaOrchestrator(question, dataProductCoordinates, metadata, {
219
+ config: configForRequest,
220
+ plugin,
221
+ history,
222
+ setMessages,
223
+ }, pureExecutionContext)
224
+ .catch(noop)
1068
225
  .finally(() => {
1069
226
  setIsSending(false);
1070
227
  });
@@ -1072,6 +229,7 @@ export const useLegendAIChatState = (services, coordinates, config, metadata, pl
1072
229
  isSending,
1073
230
  messages,
1074
231
  config,
232
+ configForRequest,
1075
233
  metadata,
1076
234
  plugin,
1077
235
  dataProductCoordinates,
@@ -1082,6 +240,9 @@ export const useLegendAIChatState = (services, coordinates, config, metadata, pl
1082
240
  setQuestionText,
1083
241
  isSending,
1084
242
  messages,
243
+ selectedModelName,
244
+ availableModelNames,
245
+ setSelectedModelName,
1085
246
  askQuestion,
1086
247
  askQuestionWithIntent,
1087
248
  runFallbackAction,
@@ -1089,6 +250,10 @@ export const useLegendAIChatState = (services, coordinates, config, metadata, pl
1089
250
  expandedThinking,
1090
251
  toggleThinking,
1091
252
  conversationRef,
253
+ selectedScopes,
254
+ toggleScope,
255
+ removeScope,
256
+ stopGeneration,
1092
257
  };
1093
258
  };
1094
259
  //# sourceMappingURL=LegendAIChatState.js.map