@finos/legend-lego 2.0.202 → 2.0.204
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/index.css +2 -2
- package/lib/index.css.map +1 -1
- package/lib/legend-ai/LegendAIDocEnrichment.d.ts +34 -1
- package/lib/legend-ai/LegendAIDocEnrichment.d.ts.map +1 -1
- package/lib/legend-ai/LegendAIDocEnrichment.js +890 -1
- package/lib/legend-ai/LegendAIDocEnrichment.js.map +1 -1
- package/lib/legend-ai/LegendAIServiceRetrieval.d.ts +36 -0
- package/lib/legend-ai/LegendAIServiceRetrieval.d.ts.map +1 -0
- package/lib/legend-ai/LegendAIServiceRetrieval.js +179 -0
- package/lib/legend-ai/LegendAIServiceRetrieval.js.map +1 -0
- package/lib/legend-ai/LegendAITypes.d.ts +62 -2
- package/lib/legend-ai/LegendAITypes.d.ts.map +1 -1
- package/lib/legend-ai/LegendAITypes.js +56 -30
- package/lib/legend-ai/LegendAITypes.js.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts +29 -6
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js +9 -0
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js.map +1 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.d.ts +5 -0
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.d.ts.map +1 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.js +18 -2
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.js.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisPanel.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisPanel.js +27 -8
- package/lib/legend-ai/components/LegendAIAnalysisPanel.js.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts +1 -0
- package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisUtils.js +144 -22
- package/lib/legend-ai/components/LegendAIAnalysisUtils.js.map +1 -1
- package/lib/legend-ai/components/LegendAICharts.d.ts +4 -0
- package/lib/legend-ai/components/LegendAICharts.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAICharts.js +60 -1
- package/lib/legend-ai/components/LegendAICharts.js.map +1 -1
- package/lib/legend-ai/components/LegendAIChat.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIChat.js +46 -17
- package/lib/legend-ai/components/LegendAIChat.js.map +1 -1
- package/lib/legend-ai/components/LegendAIChatHelpers.d.ts +1 -1
- package/lib/legend-ai/components/LegendAIChatHelpers.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIChatHelpers.js +220 -15
- package/lib/legend-ai/components/LegendAIChatHelpers.js.map +1 -1
- package/lib/legend-ai/components/LegendAIChatInput.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIChatInput.js +1 -0
- package/lib/legend-ai/components/LegendAIChatInput.js.map +1 -1
- package/lib/legend-ai/components/LegendAIScopeSelector.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIScopeSelector.js +5 -2
- package/lib/legend-ai/components/LegendAIScopeSelector.js.map +1 -1
- package/lib/legend-ai/index.d.ts +4 -3
- package/lib/legend-ai/index.d.ts.map +1 -1
- package/lib/legend-ai/index.js +4 -3
- package/lib/legend-ai/index.js.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts +30 -13
- package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatProcessors.js +776 -216
- package/lib/legend-ai/stores/LegendAIChatProcessors.js.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatState.d.ts +2 -2
- package/lib/legend-ai/stores/LegendAIChatState.d.ts.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatState.js +26 -25
- package/lib/legend-ai/stores/LegendAIChatState.js.map +1 -1
- package/package.json +5 -5
- package/src/legend-ai/LegendAIDocEnrichment.ts +1198 -1
- package/src/legend-ai/LegendAIServiceRetrieval.ts +228 -0
- package/src/legend-ai/LegendAITypes.ts +138 -32
- package/src/legend-ai/LegendAI_LegendApplicationPlugin_Extension.ts +62 -0
- package/src/legend-ai/__test-utils__/LegendAITestUtils.ts +28 -0
- package/src/legend-ai/components/LegendAIAnalysisPanel.tsx +36 -17
- package/src/legend-ai/components/LegendAIAnalysisUtils.ts +178 -22
- package/src/legend-ai/components/LegendAICharts.tsx +160 -0
- package/src/legend-ai/components/LegendAIChat.tsx +107 -14
- package/src/legend-ai/components/LegendAIChatHelpers.ts +334 -20
- package/src/legend-ai/components/LegendAIChatInput.tsx +2 -1
- package/src/legend-ai/components/LegendAIScopeSelector.tsx +7 -2
- package/src/legend-ai/index.ts +22 -1
- package/src/legend-ai/stores/LegendAIChatProcessors.ts +1205 -321
- package/src/legend-ai/stores/LegendAIChatState.ts +80 -69
- package/tsconfig.json +1 -0
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type React from 'react';
|
|
18
|
-
import { assertErrorThrown, uuid } from '@finos/legend-shared';
|
|
18
|
+
import { assertErrorThrown, isPlainObject, uuid } from '@finos/legend-shared';
|
|
19
19
|
import {
|
|
20
20
|
type TDSServiceSchema,
|
|
21
21
|
type LegendAIConfig,
|
|
@@ -24,12 +24,13 @@ import {
|
|
|
24
24
|
type LegendAIMessage,
|
|
25
25
|
type LegendAIConversationTurn,
|
|
26
26
|
type LegendAIProductMetadata,
|
|
27
|
-
|
|
27
|
+
type LegendAIModelContext,
|
|
28
28
|
LegendAIQuestionIntent,
|
|
29
29
|
LegendAIThinkingStepStatus,
|
|
30
30
|
LegendAIMessageRole,
|
|
31
31
|
LegendAIErrorType,
|
|
32
32
|
LegendAIServiceError,
|
|
33
|
+
LegendAIUnsupportedEngineShapeError,
|
|
33
34
|
TDSServiceSourceType,
|
|
34
35
|
buildColumnDefsFromNames,
|
|
35
36
|
LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
|
|
@@ -46,20 +47,40 @@ import {
|
|
|
46
47
|
type QueryExplicitExecutionContextInfo,
|
|
47
48
|
extractElementNameFromPath,
|
|
48
49
|
} from '@finos/legend-graph';
|
|
50
|
+
import {
|
|
51
|
+
buildEnrichedBusinessContext,
|
|
52
|
+
buildModelContextEnrichmentText,
|
|
53
|
+
findBestAlternateRoot,
|
|
54
|
+
splitIdentifierTokens,
|
|
55
|
+
tokenizeText,
|
|
56
|
+
} from '../LegendAIDocEnrichment.js';
|
|
57
|
+
import {
|
|
58
|
+
isFuzzyMatch,
|
|
59
|
+
preFilterServicesByRelevance,
|
|
60
|
+
} from '../LegendAIServiceRetrieval.js';
|
|
61
|
+
import {
|
|
62
|
+
isNumericColumn,
|
|
63
|
+
isStringColumn,
|
|
64
|
+
} from '../components/LegendAIChatHelpers.js';
|
|
49
65
|
|
|
50
66
|
const MAX_ERROR_MESSAGE_LENGTH = 500;
|
|
51
67
|
const MAX_THINKING_ERROR_PREVIEW_LENGTH = 200;
|
|
52
68
|
const DEFAULT_MAX_JUDGE_ATTEMPTS = 5;
|
|
53
69
|
const DEFAULT_MAX_EXECUTION_RETRIES = 3;
|
|
54
70
|
const ANALYSIS_TIMEOUT_MS = 15_000;
|
|
71
|
+
const ORCHESTRATOR_GENERATION_TIMEOUT_MS = 120_000;
|
|
72
|
+
const EXECUTION_TIMEOUT_MS = 300_000;
|
|
55
73
|
const ANALYSIS_PREVIEW_ROW_LIMIT = 3;
|
|
56
74
|
const ANALYSIS_PREVIEW_VALUE_LIMIT = 40;
|
|
57
75
|
const MAX_NON_SQL_PASS_ATTEMPTS = 2;
|
|
58
|
-
const ALIAS_DOT_COL_PATTERN = /\b(?<tbl>[a-z]\w*)\s*\.\s*"(?<col>[^"]+)"/gi;
|
|
59
76
|
const JOIN_PATTERN = /\bJOIN\b/i;
|
|
60
77
|
const ORDER_BY_SPLIT = /\bORDER\s+BY\b/i;
|
|
61
78
|
const UNION_ALL_PATTERN = /\bUNION\s+ALL\b/i;
|
|
62
79
|
const LITERAL_COL_PATTERN = /,\s*'[^']*'\s+AS\s+(?:"[^"]+"|[a-z]\w*)/gi;
|
|
80
|
+
const SELECT_ALIAS_PATTERN =
|
|
81
|
+
/\b(?<tbl>[a-z]\w*)\s*\.\s*"(?<col>[^"]+)"\s+AS\s+(?:"(?<qAlias>[^"]+)"|(?<uAlias>\w+))/gi;
|
|
82
|
+
const ALIAS_DOT_COL_PATTERN = /\b(?<tbl>[a-z]\w*)\s*\.\s*"(?<col>[^"]+)"/gi;
|
|
83
|
+
const SERVICE_CALL_PATTERN = /\bservice\s*\([^()]*\)/gi;
|
|
63
84
|
const DEFAULT_SAFETY_LIMIT = 1000;
|
|
64
85
|
const HAS_LIMIT_PATTERN = /\bLIMIT\s+\d+/i;
|
|
65
86
|
const HAS_AGGREGATION_PATTERN =
|
|
@@ -75,6 +96,7 @@ const SERVICE_PARAM_DATE_LIKE_PATTERNS: readonly RegExp[] = [
|
|
|
75
96
|
/effective|valid|settle|trade|maturity|expir|inception|close|open/i,
|
|
76
97
|
/start|end|from|until|begin|report|cutoff|valuation|pricing/i,
|
|
77
98
|
];
|
|
99
|
+
const GENERIC_TABLE_PATTERNS = /combined|consolidated|all|master|summary/i;
|
|
78
100
|
|
|
79
101
|
function isLikelySqlQuery(text: string): boolean {
|
|
80
102
|
const trimmed = text.trim().toLowerCase();
|
|
@@ -143,6 +165,7 @@ export function createMessagePair(
|
|
|
143
165
|
isExecuting: false,
|
|
144
166
|
suggestedQueries: [],
|
|
145
167
|
fallbackAction: null,
|
|
168
|
+
queriedAccessPointGroups: [],
|
|
146
169
|
},
|
|
147
170
|
];
|
|
148
171
|
}
|
|
@@ -207,6 +230,64 @@ export function classifyError(error: Error): LegendAIErrorType {
|
|
|
207
230
|
return LegendAIErrorType.GENERAL;
|
|
208
231
|
}
|
|
209
232
|
|
|
233
|
+
export enum ExecutionErrorCategory {
|
|
234
|
+
INFRASTRUCTURE = 'INFRASTRUCTURE',
|
|
235
|
+
SQL_FIXABLE = 'SQL_FIXABLE',
|
|
236
|
+
ACCESS = 'ACCESS',
|
|
237
|
+
NONE = 'NONE',
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const EXECUTION_ERROR_RULES: ReadonlyArray<{
|
|
241
|
+
category: ExecutionErrorCategory;
|
|
242
|
+
patterns: readonly RegExp[];
|
|
243
|
+
}> = [
|
|
244
|
+
{
|
|
245
|
+
category: ExecutionErrorCategory.ACCESS,
|
|
246
|
+
patterns: [
|
|
247
|
+
/\binsufficient privileges\b/i,
|
|
248
|
+
/\baccess denied\b/i,
|
|
249
|
+
/\bpermission denied\b/i,
|
|
250
|
+
/\bunauthorized\b/i,
|
|
251
|
+
/\bentitlement\b/i,
|
|
252
|
+
],
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
category: ExecutionErrorCategory.INFRASTRUCTURE,
|
|
256
|
+
patterns: [/__lake_action/i, /__lake/i],
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
category: ExecutionErrorCategory.SQL_FIXABLE,
|
|
260
|
+
patterns: [
|
|
261
|
+
/\binvalid identifier\b/i,
|
|
262
|
+
/\bambiguous column\b/i,
|
|
263
|
+
/\bdoes not exist\b/i,
|
|
264
|
+
/\bsql compilation error\b/i,
|
|
265
|
+
/\bnot found\b/i,
|
|
266
|
+
],
|
|
267
|
+
},
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
export function categorizeExecutionError(
|
|
271
|
+
errMsg: string,
|
|
272
|
+
error?: Error,
|
|
273
|
+
): ExecutionErrorCategory {
|
|
274
|
+
if (error instanceof LegendAIUnsupportedEngineShapeError) {
|
|
275
|
+
return ExecutionErrorCategory.SQL_FIXABLE;
|
|
276
|
+
}
|
|
277
|
+
if (
|
|
278
|
+
error instanceof LegendAIServiceError &&
|
|
279
|
+
error.errorType === LegendAIErrorType.PERMISSION
|
|
280
|
+
) {
|
|
281
|
+
return ExecutionErrorCategory.ACCESS;
|
|
282
|
+
}
|
|
283
|
+
for (const rule of EXECUTION_ERROR_RULES) {
|
|
284
|
+
if (rule.patterns.some((pattern) => pattern.test(errMsg))) {
|
|
285
|
+
return rule.category;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return ExecutionErrorCategory.NONE;
|
|
289
|
+
}
|
|
290
|
+
|
|
210
291
|
export function finishWithThinkingError(
|
|
211
292
|
setMessages: MessageSetter,
|
|
212
293
|
errorMsg: string,
|
|
@@ -236,7 +317,9 @@ function buildTurnFromAssistant(
|
|
|
236
317
|
sql: asstMsg.sql,
|
|
237
318
|
intent: LegendAIQuestionIntent.DATA_QUERY,
|
|
238
319
|
};
|
|
239
|
-
if (asstMsg.
|
|
320
|
+
if (asstMsg.error) {
|
|
321
|
+
turn.resultSummary = `ERROR: ${asstMsg.error.slice(0, 200)}`;
|
|
322
|
+
} else if (asstMsg.gridData) {
|
|
240
323
|
turn.rowCount = asstMsg.gridData.rowData.length;
|
|
241
324
|
const colNames = asstMsg.gridData.columnDefs
|
|
242
325
|
.map((c) => c.headerName ?? c.colId ?? '')
|
|
@@ -254,6 +337,14 @@ function buildTurnFromAssistant(
|
|
|
254
337
|
intent: LegendAIQuestionIntent.METADATA,
|
|
255
338
|
};
|
|
256
339
|
}
|
|
340
|
+
if (asstMsg.error && !asstMsg.sql) {
|
|
341
|
+
return {
|
|
342
|
+
question: userText,
|
|
343
|
+
sql: '(generation failed)',
|
|
344
|
+
resultSummary: `ERROR: ${asstMsg.error.slice(0, 200)}`,
|
|
345
|
+
intent: LegendAIQuestionIntent.DATA_QUERY,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
257
348
|
return undefined;
|
|
258
349
|
}
|
|
259
350
|
|
|
@@ -323,7 +414,21 @@ export function buildGenerationFailureMessage(
|
|
|
323
414
|
}
|
|
324
415
|
|
|
325
416
|
function buildFallbackSuggestions(services: TDSServiceSchema[]): string[] {
|
|
326
|
-
|
|
417
|
+
const result: string[] = [];
|
|
418
|
+
for (const svc of services.slice(0, 3)) {
|
|
419
|
+
const strCol = svc.columns.find(isStringColumn);
|
|
420
|
+
const numCol = svc.columns.find(isNumericColumn);
|
|
421
|
+
if (numCol && strCol) {
|
|
422
|
+
result.push(
|
|
423
|
+
`What are the top 10 ${strCol.name} values by ${numCol.name} in ${svc.title}?`,
|
|
424
|
+
);
|
|
425
|
+
} else if (strCol) {
|
|
426
|
+
result.push(`Show the breakdown by ${strCol.name} in ${svc.title}`);
|
|
427
|
+
} else {
|
|
428
|
+
result.push(`Show 10 records from ${svc.title}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return result;
|
|
327
432
|
}
|
|
328
433
|
|
|
329
434
|
function appendFallbackSuggestions(
|
|
@@ -338,6 +443,39 @@ function appendFallbackSuggestions(
|
|
|
338
443
|
}
|
|
339
444
|
}
|
|
340
445
|
|
|
446
|
+
function buildSnowflakeSqlError(
|
|
447
|
+
rawError: string,
|
|
448
|
+
services: TDSServiceSchema[],
|
|
449
|
+
): string {
|
|
450
|
+
const lowerError = rawError.toLowerCase();
|
|
451
|
+
const coreMatch =
|
|
452
|
+
/SnowflakeSQLException:\s*(?<core>SQL compilation error[^"]*?)(?:\\n|\n|$)/i.exec(
|
|
453
|
+
rawError,
|
|
454
|
+
);
|
|
455
|
+
const coreMsg = coreMatch?.groups?.core?.trim() ?? 'SQL compilation error';
|
|
456
|
+
if (lowerError.includes('__lake_action') || lowerError.includes('__lake')) {
|
|
457
|
+
return [
|
|
458
|
+
`Lakehouse SQL execution failed: ${coreMsg}`,
|
|
459
|
+
'\nThis is an internal engine error with this access point — it may not be fully supported yet.',
|
|
460
|
+
'\nTry querying a different access point, or contact the data product team.',
|
|
461
|
+
].join('');
|
|
462
|
+
}
|
|
463
|
+
if (
|
|
464
|
+
lowerError.includes('invalid identifier') ||
|
|
465
|
+
lowerError.includes('does not exist')
|
|
466
|
+
) {
|
|
467
|
+
const svcCols = services.map(
|
|
468
|
+
(s) => `${s.title}: ${s.columns.map((c) => c.name).join(', ')}`,
|
|
469
|
+
);
|
|
470
|
+
return [
|
|
471
|
+
`Lakehouse SQL execution failed: ${coreMsg}`,
|
|
472
|
+
`\nAvailable columns:\n${svcCols.join('\n')}`,
|
|
473
|
+
'\nTry rephrasing your question so the AI can pick the correct columns.',
|
|
474
|
+
].join('');
|
|
475
|
+
}
|
|
476
|
+
return `Lakehouse SQL execution failed: ${coreMsg}`;
|
|
477
|
+
}
|
|
478
|
+
|
|
341
479
|
export function buildExecutionErrorMessage(
|
|
342
480
|
errStr: string,
|
|
343
481
|
services: TDSServiceSchema[],
|
|
@@ -367,7 +505,30 @@ export function buildExecutionErrorMessage(
|
|
|
367
505
|
return errParts.join('');
|
|
368
506
|
}
|
|
369
507
|
|
|
508
|
+
if (errLower.includes('rename(~')) {
|
|
509
|
+
return [
|
|
510
|
+
'Cross-access-point JOINs on columns with the same name are not yet supported by the execution engine.',
|
|
511
|
+
'\nTry querying each access point separately, or ask a metadata question instead.',
|
|
512
|
+
].join('');
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (
|
|
516
|
+
/can't find a match for function '\w+\(Timestamp/i.test(errStr) &&
|
|
517
|
+
errLower.includes('string')
|
|
518
|
+
) {
|
|
519
|
+
return [
|
|
520
|
+
'Date comparison type mismatch: a Timestamp column was compared with a plain string literal.',
|
|
521
|
+
'\nPlease rephrase your question so the AI can regenerate the query with the correct date format.',
|
|
522
|
+
].join('');
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Snowflake SQL compilation errors — extract the core message from the trace
|
|
526
|
+
if (errLower.includes('snowflakesqlexception: sql compilation error')) {
|
|
527
|
+
return buildSnowflakeSqlError(errStr, services);
|
|
528
|
+
}
|
|
529
|
+
|
|
370
530
|
errParts.push(errStr.slice(0, MAX_ERROR_MESSAGE_LENGTH));
|
|
531
|
+
|
|
371
532
|
if (
|
|
372
533
|
errLower.includes('column') &&
|
|
373
534
|
(errLower.includes('not found') ||
|
|
@@ -408,11 +569,55 @@ function parseSuggestedQueries(rawAnswer: string): {
|
|
|
408
569
|
return { answer, suggestedQueries };
|
|
409
570
|
}
|
|
410
571
|
|
|
572
|
+
const COVERAGE_STOPWORDS = new Set([
|
|
573
|
+
'show',
|
|
574
|
+
'give',
|
|
575
|
+
'join',
|
|
576
|
+
'combine',
|
|
577
|
+
'merge',
|
|
578
|
+
'columns',
|
|
579
|
+
'column',
|
|
580
|
+
'results',
|
|
581
|
+
'result',
|
|
582
|
+
'data',
|
|
583
|
+
'services',
|
|
584
|
+
'service',
|
|
585
|
+
'access',
|
|
586
|
+
'points',
|
|
587
|
+
'point',
|
|
588
|
+
'table',
|
|
589
|
+
'tables',
|
|
590
|
+
'query',
|
|
591
|
+
'rows',
|
|
592
|
+
'records',
|
|
593
|
+
'values',
|
|
594
|
+
'list',
|
|
595
|
+
'display',
|
|
596
|
+
'fetch',
|
|
597
|
+
'retrieve',
|
|
598
|
+
'return',
|
|
599
|
+
'from',
|
|
600
|
+
'with',
|
|
601
|
+
'that',
|
|
602
|
+
'this',
|
|
603
|
+
'what',
|
|
604
|
+
'which',
|
|
605
|
+
'where',
|
|
606
|
+
'have',
|
|
607
|
+
'each',
|
|
608
|
+
'both',
|
|
609
|
+
'first',
|
|
610
|
+
'last',
|
|
611
|
+
'some',
|
|
612
|
+
'available',
|
|
613
|
+
'possible',
|
|
614
|
+
]);
|
|
615
|
+
|
|
411
616
|
function tokenizeQuestionForCoverage(question: string): string[] {
|
|
412
|
-
return question
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
617
|
+
return tokenizeText(question, {
|
|
618
|
+
minLength: 4,
|
|
619
|
+
stopwords: COVERAGE_STOPWORDS,
|
|
620
|
+
});
|
|
416
621
|
}
|
|
417
622
|
|
|
418
623
|
function buildQuestionCoverageNote(
|
|
@@ -447,9 +652,51 @@ function formatPreviewValue(value: unknown): string {
|
|
|
447
652
|
return `${raw.slice(0, ANALYSIS_PREVIEW_VALUE_LIMIT)}...`;
|
|
448
653
|
}
|
|
449
654
|
|
|
655
|
+
function buildJoinDiagnosticNote(
|
|
656
|
+
query: string,
|
|
657
|
+
columns: string[],
|
|
658
|
+
rows: unknown[],
|
|
659
|
+
): string {
|
|
660
|
+
if (!/\bJOIN\b/i.test(query) || rows.length === 0 || columns.length < 2) {
|
|
661
|
+
return '';
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const allNullColumns: string[] = [];
|
|
665
|
+
const someDataColumns: string[] = [];
|
|
666
|
+
for (const col of columns) {
|
|
667
|
+
const allNull = rows.every((row) => {
|
|
668
|
+
if (isPlainObject(row)) {
|
|
669
|
+
const val = row[col];
|
|
670
|
+
return val === null || val === undefined;
|
|
671
|
+
}
|
|
672
|
+
return true;
|
|
673
|
+
});
|
|
674
|
+
if (allNull) {
|
|
675
|
+
allNullColumns.push(col);
|
|
676
|
+
} else {
|
|
677
|
+
someDataColumns.push(col);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (
|
|
682
|
+
allNullColumns.length > 0 &&
|
|
683
|
+
someDataColumns.length > 0 &&
|
|
684
|
+
allNullColumns.length >= 2
|
|
685
|
+
) {
|
|
686
|
+
return (
|
|
687
|
+
`Note: ${allNullColumns.length} columns (${allNullColumns.slice(0, 3).join(', ')}${allNullColumns.length > 3 ? ', ...' : ''}) ` +
|
|
688
|
+
'returned entirely NULL values. This typically means the joined services do not share ' +
|
|
689
|
+
'overlapping values for the join key. The services may track different records. ' +
|
|
690
|
+
'Try querying each service separately to explore their data independently.'
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return '';
|
|
695
|
+
}
|
|
696
|
+
|
|
450
697
|
function buildDeterministicResultSummary(
|
|
451
698
|
question: string,
|
|
452
|
-
|
|
699
|
+
query: string,
|
|
453
700
|
columns: string[],
|
|
454
701
|
rows: unknown[],
|
|
455
702
|
): string {
|
|
@@ -458,11 +705,8 @@ function buildDeterministicResultSummary(
|
|
|
458
705
|
const previewRows = rows
|
|
459
706
|
.slice(0, ANALYSIS_PREVIEW_ROW_LIMIT)
|
|
460
707
|
.map((row, index) => {
|
|
461
|
-
if (
|
|
462
|
-
const entries = Object.entries(row
|
|
463
|
-
0,
|
|
464
|
-
4,
|
|
465
|
-
);
|
|
708
|
+
if (isPlainObject(row)) {
|
|
709
|
+
const entries = Object.entries(row).slice(0, 4);
|
|
466
710
|
const formattedEntries = entries.map(
|
|
467
711
|
([key, value]) => `${key}: ${formatPreviewValue(value)}`,
|
|
468
712
|
);
|
|
@@ -473,11 +717,15 @@ function buildDeterministicResultSummary(
|
|
|
473
717
|
.join('\n');
|
|
474
718
|
|
|
475
719
|
const coverageNote = buildQuestionCoverageNote(question, columns);
|
|
720
|
+
const joinDiagnostic = buildJoinDiagnosticNote(query, columns, rows);
|
|
476
721
|
const parts = [
|
|
477
722
|
`I retrieved ${rowCount} row${rowCount === 1 ? '' : 's'} for your question using this query.`,
|
|
478
723
|
`Columns returned: ${selectedColumns}.`,
|
|
479
724
|
`Sample rows:\n${previewRows || 'No sample rows available.'}`,
|
|
480
725
|
];
|
|
726
|
+
if (joinDiagnostic) {
|
|
727
|
+
parts.push(joinDiagnostic);
|
|
728
|
+
}
|
|
481
729
|
if (coverageNote) {
|
|
482
730
|
parts.push(coverageNote);
|
|
483
731
|
}
|
|
@@ -536,6 +784,8 @@ export async function handleMetadataQuestion(
|
|
|
536
784
|
context: LegendAIOperationContext,
|
|
537
785
|
startTime: number,
|
|
538
786
|
hasQueryableServices?: boolean,
|
|
787
|
+
services?: TDSServiceSchema[],
|
|
788
|
+
modelContextEnrichment?: string,
|
|
539
789
|
): Promise<void> {
|
|
540
790
|
const { config, plugin, history, setMessages } = context;
|
|
541
791
|
addThinkingStep(setMessages, 'Answering from product metadata...');
|
|
@@ -543,6 +793,8 @@ export async function handleMetadataQuestion(
|
|
|
543
793
|
question,
|
|
544
794
|
metadata,
|
|
545
795
|
history,
|
|
796
|
+
services,
|
|
797
|
+
modelContextEnrichment,
|
|
546
798
|
);
|
|
547
799
|
const rawAnswer = await plugin.callLLM(metadataPromptText, config);
|
|
548
800
|
const { answer, suggestedQueries: parsedSuggestions } =
|
|
@@ -668,6 +920,7 @@ export async function generateAndJudgeSql(
|
|
|
668
920
|
context: LegendAIOperationContext,
|
|
669
921
|
startTime: number,
|
|
670
922
|
metadata?: LegendAIProductMetadata,
|
|
923
|
+
modelContextEnrichment?: string,
|
|
671
924
|
): Promise<string | null> {
|
|
672
925
|
const { plugin, config, history, setMessages } = context;
|
|
673
926
|
addThinkingStep(setMessages, 'Building context from service schemas...');
|
|
@@ -677,6 +930,7 @@ export async function generateAndJudgeSql(
|
|
|
677
930
|
coordinates,
|
|
678
931
|
history,
|
|
679
932
|
metadata,
|
|
933
|
+
modelContextEnrichment,
|
|
680
934
|
);
|
|
681
935
|
addThinkingStep(setMessages, 'Generating SQL query...');
|
|
682
936
|
|
|
@@ -727,13 +981,16 @@ export async function generateAndJudgeAccessPointSql(
|
|
|
727
981
|
accessPoints: TDSServiceSchema[],
|
|
728
982
|
context: LegendAIOperationContext,
|
|
729
983
|
startTime: number,
|
|
984
|
+
modelContextEnrichment?: string,
|
|
730
985
|
): Promise<string | null> {
|
|
731
986
|
const { plugin, config, history, setMessages } = context;
|
|
732
987
|
addThinkingStep(setMessages, 'Building context from access point schemas...');
|
|
988
|
+
plugin.preWarmSchemaAnalysis(accessPoints, config);
|
|
733
989
|
const prompt = plugin.buildAccessPointGeneratorPrompt(
|
|
734
990
|
question,
|
|
735
991
|
accessPoints,
|
|
736
992
|
history,
|
|
993
|
+
modelContextEnrichment,
|
|
737
994
|
);
|
|
738
995
|
addThinkingStep(setMessages, 'Generating SQL query for access points...');
|
|
739
996
|
|
|
@@ -769,7 +1026,13 @@ export async function generateAndJudgeAccessPointSql(
|
|
|
769
1026
|
return runJudgeLoop(
|
|
770
1027
|
generatedSql,
|
|
771
1028
|
(sql) =>
|
|
772
|
-
plugin.buildAccessPointJudgePrompt(
|
|
1029
|
+
plugin.buildAccessPointJudgePrompt(
|
|
1030
|
+
sql,
|
|
1031
|
+
question,
|
|
1032
|
+
accessPoints,
|
|
1033
|
+
history,
|
|
1034
|
+
modelContextEnrichment,
|
|
1035
|
+
),
|
|
773
1036
|
context,
|
|
774
1037
|
);
|
|
775
1038
|
}
|
|
@@ -810,13 +1073,13 @@ export async function executeSqlAndReport(
|
|
|
810
1073
|
): Promise<LegendAISqlExecutionResultData | undefined> {
|
|
811
1074
|
const execStartTime = Date.now();
|
|
812
1075
|
try {
|
|
813
|
-
const
|
|
814
|
-
|
|
1076
|
+
const rawResult = await executeSqlForServices(
|
|
1077
|
+
sql,
|
|
1078
|
+
services,
|
|
1079
|
+
dataProductCoordinates,
|
|
1080
|
+
plugin,
|
|
1081
|
+
config,
|
|
815
1082
|
);
|
|
816
|
-
const rawResult =
|
|
817
|
-
isAccessPoint && dataProductCoordinates
|
|
818
|
-
? await plugin.executeLakehouseSql(sql, dataProductCoordinates, config)
|
|
819
|
-
: await plugin.executeSql(sql, config);
|
|
820
1083
|
return reportExecutionResult(
|
|
821
1084
|
rawResult,
|
|
822
1085
|
setMessages,
|
|
@@ -846,6 +1109,30 @@ export async function executeSqlAndReport(
|
|
|
846
1109
|
}
|
|
847
1110
|
}
|
|
848
1111
|
|
|
1112
|
+
const HAS_PURE_AGGREGATION_PATTERN =
|
|
1113
|
+
/->groupBy\(|->distinct\(|->count\(\)|->sum\(\)|->average\(\)|->olapGroupBy\(/;
|
|
1114
|
+
|
|
1115
|
+
const HAS_PURE_TAKE_PATTERN = /->take\(\s*\d+\s*\)/;
|
|
1116
|
+
|
|
1117
|
+
const PURE_LIMIT_PATTERN = /->limit\(\s*(?<count>\d+)\s*\)/g;
|
|
1118
|
+
|
|
1119
|
+
export function ensurePureSafetyLimit(
|
|
1120
|
+
pureQuery: string,
|
|
1121
|
+
limit: number = DEFAULT_SAFETY_LIMIT,
|
|
1122
|
+
): string {
|
|
1123
|
+
const normalized = pureQuery.replaceAll(
|
|
1124
|
+
PURE_LIMIT_PATTERN,
|
|
1125
|
+
(_match, n) => `->take(${n})`,
|
|
1126
|
+
);
|
|
1127
|
+
if (HAS_PURE_TAKE_PATTERN.test(normalized)) {
|
|
1128
|
+
return normalized;
|
|
1129
|
+
}
|
|
1130
|
+
if (HAS_PURE_AGGREGATION_PATTERN.test(normalized)) {
|
|
1131
|
+
return normalized;
|
|
1132
|
+
}
|
|
1133
|
+
return `${normalized.trimEnd()}->take(${limit})`;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
849
1136
|
export async function executePureQueryAndReport(
|
|
850
1137
|
pureQuery: string,
|
|
851
1138
|
pureExecutionContext: QueryExplicitExecutionContextInfo,
|
|
@@ -854,16 +1141,25 @@ export async function executePureQueryAndReport(
|
|
|
854
1141
|
plugin: LegendAI_LegendApplicationPlugin_Extension,
|
|
855
1142
|
setMessages: MessageSetter,
|
|
856
1143
|
startTime: number,
|
|
857
|
-
): Promise<LegendAISqlExecutionResultData> {
|
|
1144
|
+
): Promise<LegendAISqlExecutionResultData | undefined> {
|
|
858
1145
|
const execStartTime = Date.now();
|
|
1146
|
+
const safeQuery = ensurePureSafetyLimit(pureQuery);
|
|
859
1147
|
try {
|
|
860
1148
|
addThinkingStep(setMessages, 'Executing Pure query...');
|
|
861
|
-
const rawResult = await
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1149
|
+
const rawResult = await withTimeout(
|
|
1150
|
+
plugin.executePureQuery(
|
|
1151
|
+
safeQuery,
|
|
1152
|
+
pureExecutionContext,
|
|
1153
|
+
dataProductCoordinates,
|
|
1154
|
+
config,
|
|
1155
|
+
),
|
|
1156
|
+
EXECUTION_TIMEOUT_MS,
|
|
866
1157
|
);
|
|
1158
|
+
if (!rawResult) {
|
|
1159
|
+
throw new Error(
|
|
1160
|
+
'Query execution timed out after 5 minutes. The dataset may be too large — try adding more filters or reducing the result set.',
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
867
1163
|
return reportExecutionResult(
|
|
868
1164
|
rawResult,
|
|
869
1165
|
setMessages,
|
|
@@ -889,7 +1185,7 @@ export async function executePureQueryAndReport(
|
|
|
889
1185
|
: execErrorType,
|
|
890
1186
|
thinkingDuration: elapsedSeconds(startTime),
|
|
891
1187
|
}));
|
|
892
|
-
return
|
|
1188
|
+
return undefined;
|
|
893
1189
|
}
|
|
894
1190
|
}
|
|
895
1191
|
|
|
@@ -950,6 +1246,95 @@ export async function analyzeOrchestratorResults(
|
|
|
950
1246
|
}
|
|
951
1247
|
}
|
|
952
1248
|
|
|
1249
|
+
async function retryWithAlternateRoot(
|
|
1250
|
+
question: string,
|
|
1251
|
+
alternateRoot: string,
|
|
1252
|
+
resolvedEntities: LegendAIResolvedEntities,
|
|
1253
|
+
orchestratorOptions: Required<LegendAIOrchestratorOptionsParam>,
|
|
1254
|
+
metadata: LegendAIProductMetadata,
|
|
1255
|
+
context: LegendAIOperationContext,
|
|
1256
|
+
options: {
|
|
1257
|
+
startTime: number;
|
|
1258
|
+
modelContext?: LegendAIModelContext;
|
|
1259
|
+
},
|
|
1260
|
+
): Promise<boolean> {
|
|
1261
|
+
const { startTime, modelContext } = options;
|
|
1262
|
+
const { dataProductCoordinates, pureExecutionContext } = orchestratorOptions;
|
|
1263
|
+
const { config, plugin, setMessages } = context;
|
|
1264
|
+
addThinkingStep(
|
|
1265
|
+
setMessages,
|
|
1266
|
+
`No results with ${extractElementNameFromPath(resolvedEntities.rootEntity)}, retrying with ${extractElementNameFromPath(alternateRoot)}...`,
|
|
1267
|
+
);
|
|
1268
|
+
try {
|
|
1269
|
+
const retryRelated = resolvedEntities.relatedEntities.filter(
|
|
1270
|
+
(e) => e !== alternateRoot,
|
|
1271
|
+
);
|
|
1272
|
+
const retryEnriched = modelContext
|
|
1273
|
+
? buildEnrichedBusinessContext(
|
|
1274
|
+
question,
|
|
1275
|
+
alternateRoot,
|
|
1276
|
+
retryRelated,
|
|
1277
|
+
modelContext,
|
|
1278
|
+
)
|
|
1279
|
+
: undefined;
|
|
1280
|
+
const retryResponse = await withTimeout(
|
|
1281
|
+
plugin.generateQueryViaOrchestrator(
|
|
1282
|
+
{
|
|
1283
|
+
user_question: question,
|
|
1284
|
+
semantic_search_resolution_details: {
|
|
1285
|
+
data_product_coordinates: dataProductCoordinates,
|
|
1286
|
+
root_entity: alternateRoot,
|
|
1287
|
+
related_entities: retryRelated,
|
|
1288
|
+
...(retryEnriched
|
|
1289
|
+
? { enriched_business_context: retryEnriched }
|
|
1290
|
+
: {}),
|
|
1291
|
+
},
|
|
1292
|
+
},
|
|
1293
|
+
config,
|
|
1294
|
+
),
|
|
1295
|
+
ORCHESTRATOR_GENERATION_TIMEOUT_MS,
|
|
1296
|
+
);
|
|
1297
|
+
if (!retryResponse) {
|
|
1298
|
+
addThinkingStep(setMessages, 'Retry query generation timed out');
|
|
1299
|
+
return false;
|
|
1300
|
+
}
|
|
1301
|
+
const retrySafeQuery = ensurePureSafetyLimit(retryResponse.legend_query);
|
|
1302
|
+
updateLastAssistant(setMessages, () => ({
|
|
1303
|
+
sql: retrySafeQuery,
|
|
1304
|
+
sqlGenTime: elapsedSeconds(startTime, 2),
|
|
1305
|
+
isExecuting: true,
|
|
1306
|
+
}));
|
|
1307
|
+
const retryResult = await executePureQueryAndReport(
|
|
1308
|
+
retrySafeQuery,
|
|
1309
|
+
pureExecutionContext,
|
|
1310
|
+
dataProductCoordinates,
|
|
1311
|
+
config,
|
|
1312
|
+
plugin,
|
|
1313
|
+
setMessages,
|
|
1314
|
+
startTime,
|
|
1315
|
+
);
|
|
1316
|
+
if (retryResult && retryResult.rows.length > 0) {
|
|
1317
|
+
await analyzeOrchestratorResults(
|
|
1318
|
+
question,
|
|
1319
|
+
retryResponse.legend_query,
|
|
1320
|
+
retryResult,
|
|
1321
|
+
metadata,
|
|
1322
|
+
context,
|
|
1323
|
+
startTime,
|
|
1324
|
+
);
|
|
1325
|
+
return true;
|
|
1326
|
+
}
|
|
1327
|
+
return false;
|
|
1328
|
+
} catch (retryError) {
|
|
1329
|
+
assertErrorThrown(retryError);
|
|
1330
|
+
addThinkingStep(
|
|
1331
|
+
setMessages,
|
|
1332
|
+
`Retry with alternate entity failed: ${retryError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
1333
|
+
);
|
|
1334
|
+
return false;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
953
1338
|
async function handleEmptyOrchestratorResults(
|
|
954
1339
|
question: string,
|
|
955
1340
|
legendQuery: string,
|
|
@@ -957,65 +1342,34 @@ async function handleEmptyOrchestratorResults(
|
|
|
957
1342
|
metadata: LegendAIProductMetadata,
|
|
958
1343
|
resolvedEntities: LegendAIResolvedEntities,
|
|
959
1344
|
context: LegendAIOperationContext,
|
|
960
|
-
|
|
1345
|
+
options: {
|
|
1346
|
+
startTime: number;
|
|
1347
|
+
modelContext?: LegendAIModelContext;
|
|
1348
|
+
},
|
|
961
1349
|
): Promise<void> {
|
|
962
|
-
const {
|
|
1350
|
+
const { startTime, modelContext } = options;
|
|
963
1351
|
const { config, plugin, setMessages } = context;
|
|
964
1352
|
|
|
965
1353
|
if (resolvedEntities.relatedEntities.length > 0) {
|
|
966
|
-
const alternateRoot =
|
|
1354
|
+
const alternateRoot = modelContext
|
|
1355
|
+
? findBestAlternateRoot(
|
|
1356
|
+
resolvedEntities.rootEntity,
|
|
1357
|
+
resolvedEntities.relatedEntities,
|
|
1358
|
+
modelContext,
|
|
1359
|
+
)
|
|
1360
|
+
: resolvedEntities.relatedEntities[0];
|
|
967
1361
|
if (alternateRoot) {
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1362
|
+
const succeeded = await retryWithAlternateRoot(
|
|
1363
|
+
question,
|
|
1364
|
+
alternateRoot,
|
|
1365
|
+
resolvedEntities,
|
|
1366
|
+
orchestratorOptions,
|
|
1367
|
+
metadata,
|
|
1368
|
+
context,
|
|
1369
|
+
{ startTime, ...(modelContext === undefined ? {} : { modelContext }) },
|
|
971
1370
|
);
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
const retryResponse = await plugin.generateQueryViaOrchestrator(
|
|
975
|
-
{
|
|
976
|
-
user_question: question,
|
|
977
|
-
semantic_search_resolution_details: {
|
|
978
|
-
data_product_coordinates: dataProductCoordinates,
|
|
979
|
-
root_entity: alternateRoot,
|
|
980
|
-
related_entities: resolvedEntities.relatedEntities.slice(1),
|
|
981
|
-
},
|
|
982
|
-
},
|
|
983
|
-
config,
|
|
984
|
-
);
|
|
985
|
-
|
|
986
|
-
updateLastAssistant(setMessages, () => ({
|
|
987
|
-
sql: retryResponse.legend_query,
|
|
988
|
-
sqlGenTime: elapsedSeconds(startTime, 2),
|
|
989
|
-
isExecuting: true,
|
|
990
|
-
}));
|
|
991
|
-
|
|
992
|
-
const retryResult = await executePureQueryAndReport(
|
|
993
|
-
retryResponse.legend_query,
|
|
994
|
-
pureExecutionContext,
|
|
995
|
-
dataProductCoordinates,
|
|
996
|
-
config,
|
|
997
|
-
plugin,
|
|
998
|
-
setMessages,
|
|
999
|
-
startTime,
|
|
1000
|
-
);
|
|
1001
|
-
|
|
1002
|
-
if (retryResult.rows.length > 0) {
|
|
1003
|
-
await analyzeOrchestratorResults(
|
|
1004
|
-
question,
|
|
1005
|
-
retryResponse.legend_query,
|
|
1006
|
-
retryResult,
|
|
1007
|
-
metadata,
|
|
1008
|
-
context,
|
|
1009
|
-
startTime,
|
|
1010
|
-
);
|
|
1011
|
-
return;
|
|
1012
|
-
}
|
|
1013
|
-
} catch (retryError) {
|
|
1014
|
-
assertErrorThrown(retryError);
|
|
1015
|
-
addThinkingStep(
|
|
1016
|
-
setMessages,
|
|
1017
|
-
`Retry with alternate entity failed: ${retryError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
1018
|
-
);
|
|
1371
|
+
if (succeeded) {
|
|
1372
|
+
return;
|
|
1019
1373
|
}
|
|
1020
1374
|
}
|
|
1021
1375
|
}
|
|
@@ -1024,9 +1378,7 @@ async function handleEmptyOrchestratorResults(
|
|
|
1024
1378
|
setMessages,
|
|
1025
1379
|
'No results returned \u2014 building contextual guidance...',
|
|
1026
1380
|
);
|
|
1027
|
-
updateLastAssistant(setMessages, () => ({
|
|
1028
|
-
isProcessing: true,
|
|
1029
|
-
}));
|
|
1381
|
+
updateLastAssistant(setMessages, () => ({ isProcessing: true }));
|
|
1030
1382
|
const fallback = await withTimeout(
|
|
1031
1383
|
plugin.buildNoResultsFallback(question, legendQuery, metadata, config),
|
|
1032
1384
|
ANALYSIS_TIMEOUT_MS,
|
|
@@ -1042,6 +1394,98 @@ async function handleEmptyOrchestratorResults(
|
|
|
1042
1394
|
}
|
|
1043
1395
|
}
|
|
1044
1396
|
|
|
1397
|
+
async function resolveOrchestrationEntities(
|
|
1398
|
+
question: string,
|
|
1399
|
+
dataProductCoordinates: LegendAIOrchestratorDataProductCoordinates,
|
|
1400
|
+
context: LegendAIOperationContext,
|
|
1401
|
+
pureExecutionContext?: QueryExplicitExecutionContextInfo,
|
|
1402
|
+
preResolvedEntities?: LegendAIResolvedEntities,
|
|
1403
|
+
modelContext?: LegendAIModelContext,
|
|
1404
|
+
): Promise<LegendAIResolvedEntities> {
|
|
1405
|
+
const { config, plugin, setMessages } = context;
|
|
1406
|
+
if (preResolvedEntities) {
|
|
1407
|
+
addThinkingStep(
|
|
1408
|
+
setMessages,
|
|
1409
|
+
`Using pre-resolved root entity: ${extractElementNameFromPath(preResolvedEntities.rootEntity)}`,
|
|
1410
|
+
);
|
|
1411
|
+
return preResolvedEntities;
|
|
1412
|
+
}
|
|
1413
|
+
addThinkingStep(setMessages, 'Resolving entities for your query...');
|
|
1414
|
+
const resolvedEntities = await plugin.resolveEntitiesForQuery(
|
|
1415
|
+
question,
|
|
1416
|
+
dataProductCoordinates,
|
|
1417
|
+
config,
|
|
1418
|
+
pureExecutionContext,
|
|
1419
|
+
modelContext,
|
|
1420
|
+
);
|
|
1421
|
+
addThinkingStep(
|
|
1422
|
+
setMessages,
|
|
1423
|
+
`Found root entity: ${extractElementNameFromPath(resolvedEntities.rootEntity)}`,
|
|
1424
|
+
);
|
|
1425
|
+
return resolvedEntities;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
function extractUserErrorFromMessage(errorMessage: string): string {
|
|
1429
|
+
const marker = 'with error:';
|
|
1430
|
+
const idx = errorMessage.toLowerCase().indexOf(marker);
|
|
1431
|
+
if (idx !== -1) {
|
|
1432
|
+
const after = errorMessage.slice(idx + marker.length).trimStart();
|
|
1433
|
+
const newlineIdx = after.indexOf('\n');
|
|
1434
|
+
const reason = (
|
|
1435
|
+
newlineIdx === -1 ? after : after.slice(0, newlineIdx)
|
|
1436
|
+
).trim();
|
|
1437
|
+
if (reason.length > 0) {
|
|
1438
|
+
return reason.slice(0, MAX_ERROR_MESSAGE_LENGTH);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
return errorMessage.slice(0, MAX_ERROR_MESSAGE_LENGTH);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
async function handleOrchestratorError(
|
|
1445
|
+
error: Error,
|
|
1446
|
+
startTime: number,
|
|
1447
|
+
question: string,
|
|
1448
|
+
metadata: LegendAIProductMetadata,
|
|
1449
|
+
context: LegendAIOperationContext,
|
|
1450
|
+
): Promise<void> {
|
|
1451
|
+
const { config, plugin, setMessages } = context;
|
|
1452
|
+
const orchErrorType = classifyError(error);
|
|
1453
|
+
const genTime = elapsedSeconds(startTime, 2);
|
|
1454
|
+
addThinkingStep(
|
|
1455
|
+
setMessages,
|
|
1456
|
+
`Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
1457
|
+
);
|
|
1458
|
+
try {
|
|
1459
|
+
addThinkingStep(
|
|
1460
|
+
setMessages,
|
|
1461
|
+
'Building guidance from available metadata...',
|
|
1462
|
+
);
|
|
1463
|
+
const fallbackText = await withTimeout(
|
|
1464
|
+
plugin.buildFailureFallback(question, error.message, metadata, config),
|
|
1465
|
+
ANALYSIS_TIMEOUT_MS,
|
|
1466
|
+
);
|
|
1467
|
+
if (fallbackText) {
|
|
1468
|
+
completeThinkingSteps(setMessages);
|
|
1469
|
+
updateLastAssistant(setMessages, () => ({
|
|
1470
|
+
dataContext: fallbackText,
|
|
1471
|
+
sqlGenTime: genTime,
|
|
1472
|
+
error: extractUserErrorFromMessage(error.message),
|
|
1473
|
+
errorType: LegendAIErrorType.GENERATION,
|
|
1474
|
+
isProcessing: false,
|
|
1475
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
1476
|
+
}));
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
} catch (fallbackError) {
|
|
1480
|
+
assertErrorThrown(fallbackError);
|
|
1481
|
+
addThinkingStep(
|
|
1482
|
+
setMessages,
|
|
1483
|
+
`Fallback guidance failed: ${fallbackError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
finishWithThinkingError(setMessages, error.message, startTime, orchErrorType);
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1045
1489
|
export async function processQuestionViaOrchestrator(
|
|
1046
1490
|
question: string,
|
|
1047
1491
|
dataProductCoordinates: LegendAIOrchestratorDataProductCoordinates,
|
|
@@ -1049,31 +1493,20 @@ export async function processQuestionViaOrchestrator(
|
|
|
1049
1493
|
context: LegendAIOperationContext,
|
|
1050
1494
|
pureExecutionContext?: QueryExplicitExecutionContextInfo,
|
|
1051
1495
|
preResolvedEntities?: LegendAIResolvedEntities,
|
|
1496
|
+
modelContext?: LegendAIModelContext,
|
|
1052
1497
|
): Promise<void> {
|
|
1053
1498
|
const { config, plugin, setMessages } = context;
|
|
1054
1499
|
const startTime = Date.now();
|
|
1055
1500
|
|
|
1056
1501
|
try {
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
addThinkingStep(setMessages, 'Resolving entities for your query...');
|
|
1066
|
-
resolvedEntities = await plugin.resolveEntitiesForQuery(
|
|
1067
|
-
question,
|
|
1068
|
-
dataProductCoordinates,
|
|
1069
|
-
config,
|
|
1070
|
-
pureExecutionContext,
|
|
1071
|
-
);
|
|
1072
|
-
addThinkingStep(
|
|
1073
|
-
setMessages,
|
|
1074
|
-
`Found root entity: ${extractElementNameFromPath(resolvedEntities.rootEntity)}`,
|
|
1075
|
-
);
|
|
1076
|
-
}
|
|
1502
|
+
const resolvedEntities = await resolveOrchestrationEntities(
|
|
1503
|
+
question,
|
|
1504
|
+
dataProductCoordinates,
|
|
1505
|
+
context,
|
|
1506
|
+
pureExecutionContext,
|
|
1507
|
+
preResolvedEntities,
|
|
1508
|
+
modelContext,
|
|
1509
|
+
);
|
|
1077
1510
|
|
|
1078
1511
|
if (resolvedEntities.relatedEntities.length > 0) {
|
|
1079
1512
|
addThinkingStep(
|
|
@@ -1083,23 +1516,43 @@ export async function processQuestionViaOrchestrator(
|
|
|
1083
1516
|
}
|
|
1084
1517
|
|
|
1085
1518
|
addThinkingStep(setMessages, 'Generating Legend query via orchestrator...');
|
|
1086
|
-
const
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1519
|
+
const enrichedContext = modelContext
|
|
1520
|
+
? buildEnrichedBusinessContext(
|
|
1521
|
+
question,
|
|
1522
|
+
resolvedEntities.rootEntity,
|
|
1523
|
+
resolvedEntities.relatedEntities,
|
|
1524
|
+
modelContext,
|
|
1525
|
+
)
|
|
1526
|
+
: undefined;
|
|
1527
|
+
const orchestratorResponse = await withTimeout(
|
|
1528
|
+
plugin.generateQueryViaOrchestrator(
|
|
1529
|
+
{
|
|
1530
|
+
user_question: question,
|
|
1531
|
+
semantic_search_resolution_details: {
|
|
1532
|
+
data_product_coordinates: dataProductCoordinates,
|
|
1533
|
+
root_entity: resolvedEntities.rootEntity,
|
|
1534
|
+
related_entities: resolvedEntities.relatedEntities,
|
|
1535
|
+
...(enrichedContext
|
|
1536
|
+
? { enriched_business_context: enrichedContext }
|
|
1537
|
+
: {}),
|
|
1538
|
+
},
|
|
1093
1539
|
},
|
|
1094
|
-
|
|
1095
|
-
|
|
1540
|
+
config,
|
|
1541
|
+
),
|
|
1542
|
+
ORCHESTRATOR_GENERATION_TIMEOUT_MS,
|
|
1096
1543
|
);
|
|
1097
1544
|
|
|
1098
|
-
|
|
1545
|
+
if (!orchestratorResponse) {
|
|
1546
|
+
throw new Error(
|
|
1547
|
+
'Query generation timed out. The orchestrator took too long to respond. Try a simpler question.',
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
const safeQuery = ensurePureSafetyLimit(orchestratorResponse.legend_query);
|
|
1099
1552
|
completeThinkingSteps(setMessages);
|
|
1100
1553
|
updateLastAssistant(setMessages, () => ({
|
|
1101
|
-
sql:
|
|
1102
|
-
sqlGenTime:
|
|
1554
|
+
sql: safeQuery,
|
|
1555
|
+
sqlGenTime: elapsedSeconds(startTime, 2),
|
|
1103
1556
|
isExecuting: true,
|
|
1104
1557
|
isProcessing: true,
|
|
1105
1558
|
}));
|
|
@@ -1117,7 +1570,7 @@ export async function processQuestionViaOrchestrator(
|
|
|
1117
1570
|
}
|
|
1118
1571
|
|
|
1119
1572
|
const execResult = await executePureQueryAndReport(
|
|
1120
|
-
|
|
1573
|
+
safeQuery,
|
|
1121
1574
|
pureExecutionContext,
|
|
1122
1575
|
dataProductCoordinates,
|
|
1123
1576
|
config,
|
|
@@ -1126,11 +1579,15 @@ export async function processQuestionViaOrchestrator(
|
|
|
1126
1579
|
startTime,
|
|
1127
1580
|
);
|
|
1128
1581
|
|
|
1582
|
+
if (!execResult) {
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1129
1586
|
try {
|
|
1130
1587
|
if (execResult.rows.length > 0) {
|
|
1131
1588
|
await analyzeOrchestratorResults(
|
|
1132
1589
|
question,
|
|
1133
|
-
|
|
1590
|
+
safeQuery,
|
|
1134
1591
|
execResult,
|
|
1135
1592
|
metadata,
|
|
1136
1593
|
context,
|
|
@@ -1139,12 +1596,15 @@ export async function processQuestionViaOrchestrator(
|
|
|
1139
1596
|
} else {
|
|
1140
1597
|
await handleEmptyOrchestratorResults(
|
|
1141
1598
|
question,
|
|
1142
|
-
|
|
1599
|
+
safeQuery,
|
|
1143
1600
|
{ dataProductCoordinates, pureExecutionContext },
|
|
1144
1601
|
metadata,
|
|
1145
1602
|
resolvedEntities,
|
|
1146
1603
|
context,
|
|
1147
|
-
|
|
1604
|
+
{
|
|
1605
|
+
startTime,
|
|
1606
|
+
...(modelContext === undefined ? {} : { modelContext }),
|
|
1607
|
+
},
|
|
1148
1608
|
);
|
|
1149
1609
|
}
|
|
1150
1610
|
} catch (analysisError) {
|
|
@@ -1162,43 +1622,12 @@ export async function processQuestionViaOrchestrator(
|
|
|
1162
1622
|
}
|
|
1163
1623
|
} catch (error) {
|
|
1164
1624
|
assertErrorThrown(error);
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
setMessages,
|
|
1168
|
-
`Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
1169
|
-
);
|
|
1170
|
-
|
|
1171
|
-
try {
|
|
1172
|
-
addThinkingStep(
|
|
1173
|
-
setMessages,
|
|
1174
|
-
'Building guidance from available metadata...',
|
|
1175
|
-
);
|
|
1176
|
-
const fallbackText = await withTimeout(
|
|
1177
|
-
plugin.buildFailureFallback(question, error.message, metadata, config),
|
|
1178
|
-
ANALYSIS_TIMEOUT_MS,
|
|
1179
|
-
);
|
|
1180
|
-
if (fallbackText) {
|
|
1181
|
-
completeThinkingSteps(setMessages);
|
|
1182
|
-
updateLastAssistant(setMessages, () => ({
|
|
1183
|
-
dataContext: fallbackText,
|
|
1184
|
-
isProcessing: false,
|
|
1185
|
-
thinkingDuration: elapsedSeconds(startTime),
|
|
1186
|
-
}));
|
|
1187
|
-
return;
|
|
1188
|
-
}
|
|
1189
|
-
} catch (fallbackError) {
|
|
1190
|
-
assertErrorThrown(fallbackError);
|
|
1191
|
-
addThinkingStep(
|
|
1192
|
-
setMessages,
|
|
1193
|
-
`Fallback guidance failed: ${fallbackError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
1194
|
-
);
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
finishWithThinkingError(
|
|
1198
|
-
setMessages,
|
|
1199
|
-
error.message,
|
|
1625
|
+
await handleOrchestratorError(
|
|
1626
|
+
error,
|
|
1200
1627
|
startTime,
|
|
1201
|
-
|
|
1628
|
+
question,
|
|
1629
|
+
metadata,
|
|
1630
|
+
context,
|
|
1202
1631
|
);
|
|
1203
1632
|
}
|
|
1204
1633
|
}
|
|
@@ -1236,10 +1665,7 @@ export function sanitizeJoinOrderBy(sql: string): string {
|
|
|
1236
1665
|
const afterOrderBy = parts.slice(1).join('ORDER BY').replace(/^\s+/, '');
|
|
1237
1666
|
|
|
1238
1667
|
const selectAliases = new Map<string, string>();
|
|
1239
|
-
const
|
|
1240
|
-
/\b(?<tbl>[a-z]\w*)\s*\.\s*"(?<col>[^"]+)"\s+AS\s+(?:"(?<qAlias>[^"]+)"|(?<uAlias>\w+))/gi;
|
|
1241
|
-
let m: RegExpExecArray | null;
|
|
1242
|
-
while ((m = aliasRegex.exec(beforeOrderBy)) !== null) {
|
|
1668
|
+
for (const m of beforeOrderBy.matchAll(SELECT_ALIAS_PATTERN)) {
|
|
1243
1669
|
const tableAlias = (m.groups?.tbl ?? '').toLowerCase();
|
|
1244
1670
|
const colName = (m.groups?.col ?? '').toLowerCase();
|
|
1245
1671
|
const asAlias = m.groups?.qAlias ?? m.groups?.uAlias ?? '';
|
|
@@ -1250,17 +1676,25 @@ export function sanitizeJoinOrderBy(sql: string): string {
|
|
|
1250
1676
|
return sql;
|
|
1251
1677
|
}
|
|
1252
1678
|
|
|
1679
|
+
// Build a lookup of the original alias.col text → its SELECT alias replacement.
|
|
1680
|
+
// matchAll exposes typed `.groups`, so no cast is needed.
|
|
1681
|
+
const replacements = new Map<string, string>();
|
|
1682
|
+
for (const m of afterOrderBy.matchAll(ALIAS_DOT_COL_PATTERN)) {
|
|
1683
|
+
const tbl = (m.groups?.tbl ?? '').toLowerCase();
|
|
1684
|
+
const col = (m.groups?.col ?? '').toLowerCase();
|
|
1685
|
+
const alias = selectAliases.get(`${tbl}.${col}`);
|
|
1686
|
+
if (alias) {
|
|
1687
|
+
replacements.set(m[0], `"${alias}"`);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
if (replacements.size === 0) {
|
|
1692
|
+
return sql;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1253
1695
|
const rewritten = afterOrderBy.replaceAll(
|
|
1254
1696
|
ALIAS_DOT_COL_PATTERN,
|
|
1255
|
-
(
|
|
1256
|
-
const groups = args[args.length - 1] as {
|
|
1257
|
-
tbl: string;
|
|
1258
|
-
col: string;
|
|
1259
|
-
};
|
|
1260
|
-
const key = `${groups.tbl.toLowerCase()}.${groups.col.toLowerCase()}`;
|
|
1261
|
-
const alias = selectAliases.get(key);
|
|
1262
|
-
return alias ? `"${alias}"` : String(args[0]);
|
|
1263
|
-
},
|
|
1697
|
+
(match) => replacements.get(match) ?? match,
|
|
1264
1698
|
);
|
|
1265
1699
|
|
|
1266
1700
|
if (rewritten === afterOrderBy) {
|
|
@@ -1269,6 +1703,88 @@ export function sanitizeJoinOrderBy(sql: string): string {
|
|
|
1269
1703
|
return `${beforeOrderBy}ORDER BY ${rewritten}`;
|
|
1270
1704
|
}
|
|
1271
1705
|
|
|
1706
|
+
export function sanitizeJoinSameKeyColumns(
|
|
1707
|
+
sql: string,
|
|
1708
|
+
services?: TDSServiceSchema[],
|
|
1709
|
+
): string {
|
|
1710
|
+
if (!JOIN_PATTERN.test(sql)) {
|
|
1711
|
+
return sql;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
const joinRegex =
|
|
1715
|
+
/\bJOIN\s{1,5}p\(\s{0,5}'(?<pId>[^']{1,200})'\s{0,5}\)\s{1,5}AS\s{1,5}(?<rAlias>[a-z]\w{0,63})\s{1,5}ON\s{1,5}(?<onClause>[^\n]{1,500})/gi;
|
|
1716
|
+
|
|
1717
|
+
let result = sql;
|
|
1718
|
+
let match: RegExpExecArray | null;
|
|
1719
|
+
|
|
1720
|
+
while ((match = joinRegex.exec(sql)) !== null) {
|
|
1721
|
+
const pId = match.groups?.pId ?? '';
|
|
1722
|
+
const rightAlias = match.groups?.rAlias ?? '';
|
|
1723
|
+
const onClause = (match.groups?.onClause ?? '').slice(0, 500);
|
|
1724
|
+
|
|
1725
|
+
const sameKeyMatch =
|
|
1726
|
+
/(?<lAlias>[a-z]\w{0,63}) {0,5}\. {0,5}"(?<lCol>[^"]{1,200})" {0,5}= {0,5}(?<rAl>[a-z]\w{0,63}) {0,5}\. {0,5}"(?<rCol>[^"]{1,200})"/i.exec(
|
|
1727
|
+
onClause,
|
|
1728
|
+
);
|
|
1729
|
+
if (!sameKeyMatch) {
|
|
1730
|
+
continue;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
const leftCol = sameKeyMatch.groups?.lCol ?? '';
|
|
1734
|
+
const rightCol = sameKeyMatch.groups?.rCol ?? '';
|
|
1735
|
+
const matchedRightAlias = sameKeyMatch.groups?.rAl ?? '';
|
|
1736
|
+
|
|
1737
|
+
if (
|
|
1738
|
+
leftCol.toLowerCase() !== rightCol.toLowerCase() ||
|
|
1739
|
+
matchedRightAlias.toLowerCase() !== rightAlias.toLowerCase()
|
|
1740
|
+
) {
|
|
1741
|
+
continue;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
const renamedKey = `${rightAlias}_${rightCol}`;
|
|
1745
|
+
const columnList = buildSubqueryColumnList(
|
|
1746
|
+
pId,
|
|
1747
|
+
rightCol,
|
|
1748
|
+
renamedKey,
|
|
1749
|
+
services,
|
|
1750
|
+
);
|
|
1751
|
+
const originalFragment = `p('${pId}') AS ${rightAlias}`;
|
|
1752
|
+
const subqueryFragment = `(SELECT ${columnList} FROM p('${pId}')) AS ${rightAlias}`;
|
|
1753
|
+
|
|
1754
|
+
const oldOnRef = `${rightAlias}."${rightCol}"`;
|
|
1755
|
+
const newOnRef = `${rightAlias}."${renamedKey}"`;
|
|
1756
|
+
|
|
1757
|
+
result = result.replace(originalFragment, subqueryFragment);
|
|
1758
|
+
result = result.replaceAll(oldOnRef, newOnRef);
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
return result;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
function buildSubqueryColumnList(
|
|
1765
|
+
pId: string,
|
|
1766
|
+
originalKey: string,
|
|
1767
|
+
renamedKey: string,
|
|
1768
|
+
services?: TDSServiceSchema[],
|
|
1769
|
+
): string {
|
|
1770
|
+
const svc = services?.find(
|
|
1771
|
+
(s) =>
|
|
1772
|
+
s.dataProductPath &&
|
|
1773
|
+
s.pattern &&
|
|
1774
|
+
pId === `${s.dataProductPath}.${s.pattern.replace(/^\//, '')}`,
|
|
1775
|
+
);
|
|
1776
|
+
if (!svc || svc.columns.length === 0) {
|
|
1777
|
+
return `"${originalKey}" AS "${renamedKey}", *`;
|
|
1778
|
+
}
|
|
1779
|
+
return svc.columns
|
|
1780
|
+
.map((c) =>
|
|
1781
|
+
c.name.toLowerCase() === originalKey.toLowerCase()
|
|
1782
|
+
? `"${c.name}" AS "${renamedKey}"`
|
|
1783
|
+
: `"${c.name}"`,
|
|
1784
|
+
)
|
|
1785
|
+
.join(', ');
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1272
1788
|
export function sanitizeLiteralColumns(sql: string): string {
|
|
1273
1789
|
if (!UNION_ALL_PATTERN.test(sql)) {
|
|
1274
1790
|
return sql;
|
|
@@ -1347,8 +1863,7 @@ export function ensureDateParameters(
|
|
|
1347
1863
|
|
|
1348
1864
|
const today = getTodayISO();
|
|
1349
1865
|
|
|
1350
|
-
|
|
1351
|
-
return sql.replaceAll(serviceCallPattern, (match) => {
|
|
1866
|
+
return sql.replaceAll(SERVICE_CALL_PATTERN, (match) => {
|
|
1352
1867
|
let patched = match;
|
|
1353
1868
|
for (const param of dateParams) {
|
|
1354
1869
|
if (new RegExp(String.raw`\b${param}\s*=>`, 'i').test(patched)) {
|
|
@@ -1450,6 +1965,111 @@ export function buildMissingParamsWarning(
|
|
|
1450
1965
|
].join('\n');
|
|
1451
1966
|
}
|
|
1452
1967
|
|
|
1968
|
+
const NESTED_P_IN_CLAUSE = /\bIN\s*\(\s*SELECT\b[^)]*\bFROM\s+p\s*\(/i;
|
|
1969
|
+
const NESTED_P_CROSS_JOIN =
|
|
1970
|
+
/\bCROSS\s+JOIN\s*\(\s*SELECT\b[^)]*\bFROM\s+p\s*\(/i;
|
|
1971
|
+
const NESTED_P_SCALAR =
|
|
1972
|
+
/\(\s*SELECT\s+(?:COUNT|SUM|AVG|MIN|MAX)\s*\([^)]*\)\s+(?:AS\s+\w+\s+)?FROM\s+p\s*\(/i;
|
|
1973
|
+
|
|
1974
|
+
export function hasNestedPCalls(sql: string): boolean {
|
|
1975
|
+
return (
|
|
1976
|
+
NESTED_P_IN_CLAUSE.test(sql) ||
|
|
1977
|
+
NESTED_P_CROSS_JOIN.test(sql) ||
|
|
1978
|
+
NESTED_P_SCALAR.test(sql)
|
|
1979
|
+
);
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
const NESTED_AGGREGATE_PATTERN =
|
|
1983
|
+
/\b(?:SUM|AVG|MIN|MAX|COUNT)\s*\(\s*(?:SUM|AVG|MIN|MAX|COUNT)\s*\(/i;
|
|
1984
|
+
const OVER_CLAUSE_PATTERN = /\bOVER\s*\(/i;
|
|
1985
|
+
|
|
1986
|
+
const AGGREGATE_IN_WINDOW_ARGS_PATTERN =
|
|
1987
|
+
/\bOVER\s*\((?:[^()]|\([^()]*\)){0,300}\b(?:SUM|AVG|MIN|MAX|COUNT)\s*\(/i;
|
|
1988
|
+
|
|
1989
|
+
const NON_WINDOW_SCALAR_FUNCTIONS: ReadonlySet<string> = new Set([
|
|
1990
|
+
'ROUND',
|
|
1991
|
+
'CAST',
|
|
1992
|
+
'COALESCE',
|
|
1993
|
+
'CEIL',
|
|
1994
|
+
'CEILING',
|
|
1995
|
+
'FLOOR',
|
|
1996
|
+
'ABS',
|
|
1997
|
+
'CONCAT',
|
|
1998
|
+
'NULLIF',
|
|
1999
|
+
'IFNULL',
|
|
2000
|
+
'GREATEST',
|
|
2001
|
+
'LEAST',
|
|
2002
|
+
'TRUNC',
|
|
2003
|
+
'TRUNCATE',
|
|
2004
|
+
'MOD',
|
|
2005
|
+
'POWER',
|
|
2006
|
+
'SQRT',
|
|
2007
|
+
'LN',
|
|
2008
|
+
'LOG',
|
|
2009
|
+
'EXP',
|
|
2010
|
+
'SUBSTR',
|
|
2011
|
+
'SUBSTRING',
|
|
2012
|
+
'TO_CHAR',
|
|
2013
|
+
'TO_NUMBER',
|
|
2014
|
+
'TO_DATE',
|
|
2015
|
+
]);
|
|
2016
|
+
const FUNCTION_CALL_WITH_OVER_PATTERN =
|
|
2017
|
+
/\b(?<fn>[A-Z_]{1,32})\s*\((?:[^()]|\([^()]*\)){0,200}\bOVER\s*\(/gi;
|
|
2018
|
+
|
|
2019
|
+
function hasWindowInsideNonWindowFunctionCall(sql: string): boolean {
|
|
2020
|
+
FUNCTION_CALL_WITH_OVER_PATTERN.lastIndex = 0;
|
|
2021
|
+
let match: RegExpExecArray | null;
|
|
2022
|
+
while ((match = FUNCTION_CALL_WITH_OVER_PATTERN.exec(sql)) !== null) {
|
|
2023
|
+
const fn = match.groups?.fn?.toUpperCase();
|
|
2024
|
+
if (fn && NON_WINDOW_SCALAR_FUNCTIONS.has(fn)) {
|
|
2025
|
+
return true;
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
return false;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
const LATERAL_SUBQUERY_PATTERN =
|
|
2032
|
+
/\b(?:CROSS|INNER|LEFT|RIGHT)?\s*JOIN\s+LATERAL\b|\bLATERAL\s*\(/i;
|
|
2033
|
+
|
|
2034
|
+
export interface UnsupportedEnginePattern {
|
|
2035
|
+
kind:
|
|
2036
|
+
| 'NESTED_AGGREGATE_IN_WINDOW'
|
|
2037
|
+
| 'AGGREGATE_IN_WINDOW_ARGS'
|
|
2038
|
+
| 'WINDOW_INSIDE_FUNCTION_CALL'
|
|
2039
|
+
| 'LATERAL_SUBQUERY';
|
|
2040
|
+
hint: string;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
export function detectUnsupportedEnginePattern(
|
|
2044
|
+
sql: string,
|
|
2045
|
+
): UnsupportedEnginePattern | undefined {
|
|
2046
|
+
if (NESTED_AGGREGATE_PATTERN.test(sql) && OVER_CLAUSE_PATTERN.test(sql)) {
|
|
2047
|
+
return {
|
|
2048
|
+
kind: 'NESTED_AGGREGATE_IN_WINDOW',
|
|
2049
|
+
hint: "The engine's SQL→Pure translator cannot combine an aggregate function with a window function in the same SELECT (e.g. SUM(COUNT(*)) OVER ()). Move the aggregation into a CTE first, then apply the window over the CTE.",
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
if (AGGREGATE_IN_WINDOW_ARGS_PATTERN.test(sql)) {
|
|
2053
|
+
return {
|
|
2054
|
+
kind: 'AGGREGATE_IN_WINDOW_ARGS',
|
|
2055
|
+
hint: "The engine's SQL→Pure translator cannot evaluate an aggregate function inside an OVER clause's PARTITION BY or ORDER BY at the same level as GROUP BY (e.g. ROW_NUMBER() OVER (ORDER BY COUNT(*) DESC)). Materialize the aggregate into a CTE column first, then reference that column from the window — for example: SELECT col, cnt, ROW_NUMBER() OVER (ORDER BY cnt DESC) AS rn FROM (SELECT col, COUNT(*) AS cnt FROM p('…') GROUP BY col) agg.",
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
if (hasWindowInsideNonWindowFunctionCall(sql)) {
|
|
2059
|
+
return {
|
|
2060
|
+
kind: 'WINDOW_INSIDE_FUNCTION_CALL',
|
|
2061
|
+
hint: "The engine's SQL→Pure translator cannot evaluate a window function inside another function call (e.g. ROUND(SUM(x) OVER (), 2)). Materialize the window expression into its own column in a subquery or CTE, then apply the wrapping function in the outer SELECT — for example: SELECT col, ROUND(cnt * 100.0 / total, 2) FROM (SELECT col, cnt, SUM(cnt) OVER () AS total FROM (SELECT col, COUNT(*) AS cnt FROM p('…') GROUP BY col) agg) windowed.",
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
2064
|
+
if (LATERAL_SUBQUERY_PATTERN.test(sql)) {
|
|
2065
|
+
return {
|
|
2066
|
+
kind: 'LATERAL_SUBQUERY',
|
|
2067
|
+
hint: "The engine's SQL parser does not support LATERAL subqueries (CROSS JOIN LATERAL (...)). Rewrite using ROW_NUMBER() OVER (PARTITION BY ...) in a CTE and filter on the rank, or use a correlated subquery.",
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
return undefined;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
1453
2073
|
/**
|
|
1454
2074
|
* Appends a safety LIMIT to queries that lack one, preventing unbounded
|
|
1455
2075
|
* result sets on large services. Skips aggregation queries since those
|
|
@@ -1465,6 +2085,29 @@ export function ensureSafeLimit(
|
|
|
1465
2085
|
return `${sql.trimEnd()}\nLIMIT ${limit}`;
|
|
1466
2086
|
}
|
|
1467
2087
|
|
|
2088
|
+
function prepareSafeSql(sql: string, services: TDSServiceSchema[]): string {
|
|
2089
|
+
const safeSql = ensureSafeLimit(
|
|
2090
|
+
ensureDateParameters(
|
|
2091
|
+
sanitizeLiteralColumns(
|
|
2092
|
+
sanitizeJoinSameKeyColumns(sanitizeJoinOrderBy(sql), services),
|
|
2093
|
+
),
|
|
2094
|
+
services,
|
|
2095
|
+
),
|
|
2096
|
+
);
|
|
2097
|
+
const unsupported = detectUnsupportedEnginePattern(safeSql);
|
|
2098
|
+
if (unsupported) {
|
|
2099
|
+
throw new LegendAIUnsupportedEngineShapeError(unsupported.hint);
|
|
2100
|
+
}
|
|
2101
|
+
return safeSql;
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
/**
|
|
2105
|
+
* The single execution chokepoint for SQL in this module. Every code
|
|
2106
|
+
* path that needs to run SQL — primary orchestrator, retry-with-fix,
|
|
2107
|
+
* exported `executeSqlAndReport`, internal helpers — calls through
|
|
2108
|
+
* here. No other code in this file may call `plugin.executeLakehouseSql`
|
|
2109
|
+
* or `plugin.executeSql` directly.
|
|
2110
|
+
*/
|
|
1468
2111
|
async function executeSqlForServices(
|
|
1469
2112
|
sql: string,
|
|
1470
2113
|
services: TDSServiceSchema[],
|
|
@@ -1474,12 +2117,7 @@ async function executeSqlForServices(
|
|
|
1474
2117
|
plugin: LegendAI_LegendApplicationPlugin_Extension,
|
|
1475
2118
|
config: LegendAIConfig,
|
|
1476
2119
|
): Promise<LegendAISqlExecutionResultData> {
|
|
1477
|
-
const safeSql =
|
|
1478
|
-
ensureDateParameters(
|
|
1479
|
-
sanitizeLiteralColumns(sanitizeJoinOrderBy(sql)),
|
|
1480
|
-
services,
|
|
1481
|
-
),
|
|
1482
|
-
);
|
|
2120
|
+
const safeSql = prepareSafeSql(sql, services);
|
|
1483
2121
|
const isAccessPoint = services.some(
|
|
1484
2122
|
(s) => s.sourceType === TDSServiceSourceType.ACCESS_POINT,
|
|
1485
2123
|
);
|
|
@@ -1808,51 +2446,150 @@ async function reportQueryResults(
|
|
|
1808
2446
|
}
|
|
1809
2447
|
}
|
|
1810
2448
|
|
|
1811
|
-
|
|
1812
|
-
* Scores each service against the user question by counting keyword
|
|
1813
|
-
* overlap between the question tokens and the service title, description,
|
|
1814
|
-
* column names, and parameter names. Returns services sorted by
|
|
1815
|
-
* descending relevance score.
|
|
1816
|
-
*/
|
|
1817
|
-
export function preFilterServicesByRelevance(
|
|
2449
|
+
export function supplementMissingCoverage(
|
|
1818
2450
|
question: string,
|
|
1819
|
-
|
|
1820
|
-
|
|
2451
|
+
selected: TDSServiceSchema[],
|
|
2452
|
+
allServices: TDSServiceSchema[],
|
|
2453
|
+
maxTotal: number = 4,
|
|
1821
2454
|
): TDSServiceSchema[] {
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
if (
|
|
1827
|
-
return
|
|
1828
|
-
}
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
2455
|
+
if (selected.length >= maxTotal || allServices.length <= selected.length) {
|
|
2456
|
+
return selected;
|
|
2457
|
+
}
|
|
2458
|
+
const questionTokens = tokenizeText(question);
|
|
2459
|
+
if (questionTokens.length === 0) {
|
|
2460
|
+
return selected;
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
const coveredTokens = new Set<string>();
|
|
2464
|
+
for (const svc of selected) {
|
|
2465
|
+
for (const col of svc.columns) {
|
|
2466
|
+
for (const token of splitIdentifierTokens(col.name)) {
|
|
2467
|
+
coveredTokens.add(token);
|
|
2468
|
+
}
|
|
2469
|
+
coveredTokens.add(col.name.toLowerCase());
|
|
2470
|
+
}
|
|
2471
|
+
for (const token of splitIdentifierTokens(svc.title)) {
|
|
2472
|
+
coveredTokens.add(token);
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
const coveredArray = Array.from(coveredTokens);
|
|
2477
|
+
const uncovered = questionTokens.filter(
|
|
2478
|
+
(t) =>
|
|
2479
|
+
!coveredTokens.has(t) &&
|
|
2480
|
+
!(t.length >= 4 && coveredArray.some((ct) => isFuzzyMatch(t, ct))),
|
|
2481
|
+
);
|
|
2482
|
+
if (uncovered.length === 0) {
|
|
2483
|
+
return selected;
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
const selectedSet = new Set(selected.map((s) => s.pattern));
|
|
2487
|
+
const supplementCandidates = allServices
|
|
2488
|
+
.filter((svc) => !selectedSet.has(svc.pattern))
|
|
2489
|
+
.map((svc) => {
|
|
2490
|
+
const svcTokens = new Set<string>();
|
|
2491
|
+
for (const col of svc.columns) {
|
|
2492
|
+
svcTokens.add(col.name.toLowerCase());
|
|
2493
|
+
for (const token of splitIdentifierTokens(col.name)) {
|
|
2494
|
+
svcTokens.add(token);
|
|
1840
2495
|
}
|
|
1841
|
-
return parts;
|
|
1842
|
-
}),
|
|
1843
|
-
]
|
|
1844
|
-
.join(' ')
|
|
1845
|
-
.toLowerCase();
|
|
1846
|
-
let score = 0;
|
|
1847
|
-
for (const token of queryTokens) {
|
|
1848
|
-
if (haystack.includes(token)) {
|
|
1849
|
-
score += 1;
|
|
1850
2496
|
}
|
|
2497
|
+
for (const token of splitIdentifierTokens(svc.title)) {
|
|
2498
|
+
svcTokens.add(token);
|
|
2499
|
+
}
|
|
2500
|
+
const coverCount = uncovered.filter((t) => svcTokens.has(t)).length;
|
|
2501
|
+
const isGeneric = GENERIC_TABLE_PATTERNS.test(svc.title) ? 1 : 0;
|
|
2502
|
+
return { svc, coverCount, isGeneric };
|
|
2503
|
+
})
|
|
2504
|
+
.filter((c) => c.coverCount > 0)
|
|
2505
|
+
.sort((a, b) => b.coverCount - a.coverCount || a.isGeneric - b.isGeneric);
|
|
2506
|
+
|
|
2507
|
+
const result = [...selected];
|
|
2508
|
+
for (const candidate of supplementCandidates) {
|
|
2509
|
+
if (result.length >= maxTotal) {
|
|
2510
|
+
break;
|
|
1851
2511
|
}
|
|
1852
|
-
|
|
2512
|
+
result.push(candidate.svc);
|
|
2513
|
+
}
|
|
2514
|
+
return result;
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
// ─── Question Normalization ──────────────────────────────────────────────────
|
|
2518
|
+
|
|
2519
|
+
/** Delegates to the plugin's LLM to fix natural-language typos in the user's question before AP selection; falls back to the original on failure. */
|
|
2520
|
+
export async function normalizeQuestion(
|
|
2521
|
+
question: string,
|
|
2522
|
+
plugin: LegendAI_LegendApplicationPlugin_Extension,
|
|
2523
|
+
config: LegendAIConfig,
|
|
2524
|
+
): Promise<string> {
|
|
2525
|
+
try {
|
|
2526
|
+
return await plugin.normalizeQuestion(question, config);
|
|
2527
|
+
} catch (error) {
|
|
2528
|
+
assertErrorThrown(error);
|
|
2529
|
+
return question;
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
// ─── Multi-Turn AP Bias ──────────────────────────────────────────────────────
|
|
2534
|
+
|
|
2535
|
+
/**
|
|
2536
|
+
* Extracts AP patterns that were successfully used in previous conversation
|
|
2537
|
+
* turns. When the user asks a follow-up question, these APs should be
|
|
2538
|
+
* preferred since they're likely still relevant.
|
|
2539
|
+
*/
|
|
2540
|
+
function extractPreviousTurnAPPatterns(
|
|
2541
|
+
history: LegendAIConversationTurn[],
|
|
2542
|
+
): Set<string> {
|
|
2543
|
+
const patterns = new Set<string>();
|
|
2544
|
+
for (const turn of history) {
|
|
2545
|
+
const pCallPattern = /p\(\s*'(?<pId>[^']+)'\s*\)/g;
|
|
2546
|
+
let pCall: RegExpExecArray | null;
|
|
2547
|
+
while ((pCall = pCallPattern.exec(turn.sql)) !== null) {
|
|
2548
|
+
const pattern = pCall.groups?.pId;
|
|
2549
|
+
if (pattern) {
|
|
2550
|
+
const lastDot = pattern.lastIndexOf('.');
|
|
2551
|
+
if (lastDot >= 0) {
|
|
2552
|
+
patterns.add(pattern.slice(lastDot + 1).toLowerCase());
|
|
2553
|
+
}
|
|
2554
|
+
patterns.add(pattern.toLowerCase());
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
return patterns;
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
/**
|
|
2562
|
+
* Applies a scoring boost to services whose patterns appeared in previous
|
|
2563
|
+
* conversation turns, biasing the pre-filter toward continuity when the
|
|
2564
|
+
* user asks follow-up questions about the same data.
|
|
2565
|
+
*/
|
|
2566
|
+
export function applyMultiTurnBias(
|
|
2567
|
+
services: TDSServiceSchema[],
|
|
2568
|
+
history: LegendAIConversationTurn[],
|
|
2569
|
+
): TDSServiceSchema[] {
|
|
2570
|
+
if (history.length === 0) {
|
|
2571
|
+
return services;
|
|
2572
|
+
}
|
|
2573
|
+
const previousAPs = extractPreviousTurnAPPatterns(history);
|
|
2574
|
+
if (previousAPs.size === 0) {
|
|
2575
|
+
return services;
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
// Sort so that previously-used APs come first, preserving relative order
|
|
2579
|
+
const biased = [...services].sort((a, b) => {
|
|
2580
|
+
const aUsed =
|
|
2581
|
+
previousAPs.has(a.pattern.replace(/^\//, '').toLowerCase()) ||
|
|
2582
|
+
previousAPs.has(a.title.toLowerCase())
|
|
2583
|
+
? 1
|
|
2584
|
+
: 0;
|
|
2585
|
+
const bUsed =
|
|
2586
|
+
previousAPs.has(b.pattern.replace(/^\//, '').toLowerCase()) ||
|
|
2587
|
+
previousAPs.has(b.title.toLowerCase())
|
|
2588
|
+
? 1
|
|
2589
|
+
: 0;
|
|
2590
|
+
return bUsed - aUsed;
|
|
1853
2591
|
});
|
|
1854
|
-
|
|
1855
|
-
return scored.slice(0, limit).map((s) => s.svc);
|
|
2592
|
+
return biased;
|
|
1856
2593
|
}
|
|
1857
2594
|
|
|
1858
2595
|
async function selectBestServices(
|
|
@@ -1860,27 +2597,66 @@ async function selectBestServices(
|
|
|
1860
2597
|
services: TDSServiceSchema[],
|
|
1861
2598
|
context: LegendAIOperationContext,
|
|
1862
2599
|
): Promise<TDSServiceSchema[]> {
|
|
1863
|
-
const { plugin, config, setMessages } = context;
|
|
2600
|
+
const { plugin, config, setMessages, history } = context;
|
|
1864
2601
|
if (services.length <= 1) {
|
|
1865
2602
|
return services;
|
|
1866
2603
|
}
|
|
1867
2604
|
|
|
1868
|
-
|
|
1869
|
-
|
|
2605
|
+
// ── Step 0: Normalize the question (expand abbreviations, fix typos) ──
|
|
2606
|
+
addThinkingStep(setMessages, 'Normalizing question...');
|
|
2607
|
+
const normalizedQuestion = await normalizeQuestion(question, plugin, config);
|
|
2608
|
+
if (normalizedQuestion !== question) {
|
|
2609
|
+
addThinkingStep(setMessages, `Normalized: "${normalizedQuestion}"`);
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
// ── Step 0b: Apply multi-turn bias if this is a follow-up ──
|
|
2613
|
+
let inputServices = services;
|
|
2614
|
+
if (history.length > 0) {
|
|
2615
|
+
inputServices = applyMultiTurnBias(services, history);
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
let candidates = inputServices;
|
|
2619
|
+
if (inputServices.length > MAX_SERVICES_FOR_LLM_SELECTION) {
|
|
1870
2620
|
candidates = preFilterServicesByRelevance(
|
|
1871
|
-
|
|
1872
|
-
|
|
2621
|
+
normalizedQuestion,
|
|
2622
|
+
inputServices,
|
|
1873
2623
|
MAX_SERVICES_FOR_LLM_SELECTION,
|
|
1874
2624
|
);
|
|
2625
|
+
const topNames = candidates
|
|
2626
|
+
.slice(0, 5)
|
|
2627
|
+
.map((s) => `${s.title} (${s.columns.length} cols)`)
|
|
2628
|
+
.join(', ');
|
|
1875
2629
|
addThinkingStep(
|
|
1876
2630
|
setMessages,
|
|
1877
|
-
`Pre-filtered ${services.length} services to ${candidates.length} candidates`,
|
|
2631
|
+
`Pre-filtered ${services.length} services to ${candidates.length} candidates. Top: ${topNames}`,
|
|
1878
2632
|
);
|
|
1879
2633
|
}
|
|
1880
2634
|
|
|
1881
2635
|
try {
|
|
1882
2636
|
addThinkingStep(setMessages, 'Selecting best service for your query...');
|
|
1883
|
-
|
|
2637
|
+
const selected = await plugin.selectRelevantServices(
|
|
2638
|
+
normalizedQuestion,
|
|
2639
|
+
candidates,
|
|
2640
|
+
config,
|
|
2641
|
+
);
|
|
2642
|
+
const selectedNames = selected.map((s) => s.title).join(', ');
|
|
2643
|
+
addThinkingStep(setMessages, `LLM selected: ${selectedNames}`);
|
|
2644
|
+
const validated = supplementMissingCoverage(
|
|
2645
|
+
normalizedQuestion,
|
|
2646
|
+
selected,
|
|
2647
|
+
candidates,
|
|
2648
|
+
);
|
|
2649
|
+
if (validated.length > selected.length) {
|
|
2650
|
+
const supplemented = validated
|
|
2651
|
+
.slice(selected.length)
|
|
2652
|
+
.map((s) => s.title)
|
|
2653
|
+
.join(', ');
|
|
2654
|
+
addThinkingStep(
|
|
2655
|
+
setMessages,
|
|
2656
|
+
`Supplemented with ${supplemented} for column coverage`,
|
|
2657
|
+
);
|
|
2658
|
+
}
|
|
2659
|
+
return validated;
|
|
1884
2660
|
} catch (selectionError) {
|
|
1885
2661
|
assertErrorThrown(selectionError);
|
|
1886
2662
|
addThinkingStep(
|
|
@@ -1952,6 +2728,121 @@ async function tryRecoverZeroRows(
|
|
|
1952
2728
|
return { sql: recoveredSql, result: recoveredResult };
|
|
1953
2729
|
}
|
|
1954
2730
|
|
|
2731
|
+
async function resolveNestedPCalls(
|
|
2732
|
+
sql: string,
|
|
2733
|
+
question: string,
|
|
2734
|
+
selectedAPs: TDSServiceSchema[],
|
|
2735
|
+
context: LegendAIOperationContext,
|
|
2736
|
+
modelContextEnrichment?: string,
|
|
2737
|
+
): Promise<string> {
|
|
2738
|
+
if (!hasNestedPCalls(sql)) {
|
|
2739
|
+
return sql;
|
|
2740
|
+
}
|
|
2741
|
+
const { plugin, config, setMessages } = context;
|
|
2742
|
+
addThinkingStep(setMessages, 'Detected nested p() call — rewriting query...');
|
|
2743
|
+
const fixPrompt = plugin.buildAccessPointErrorCorrectionPrompt(
|
|
2744
|
+
sql,
|
|
2745
|
+
question,
|
|
2746
|
+
'The SQL contains p() inside a subquery (IN clause, CROSS JOIN, or scalar subquery). p() can ONLY appear directly after FROM or JOIN.',
|
|
2747
|
+
selectedAPs,
|
|
2748
|
+
context.history,
|
|
2749
|
+
modelContextEnrichment,
|
|
2750
|
+
);
|
|
2751
|
+
try {
|
|
2752
|
+
const fixResponse = await plugin.callLLM(fixPrompt, config);
|
|
2753
|
+
const fixResult = plugin.extractJudgeResult(fixResponse);
|
|
2754
|
+
const corrected = fixResult.correctedSql;
|
|
2755
|
+
if (corrected && !hasNestedPCalls(corrected)) {
|
|
2756
|
+
return corrected;
|
|
2757
|
+
}
|
|
2758
|
+
} catch (fixError) {
|
|
2759
|
+
assertErrorThrown(fixError);
|
|
2760
|
+
addThinkingStep(
|
|
2761
|
+
setMessages,
|
|
2762
|
+
`Could not rewrite query: ${fixError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}. Proceeding with original SQL.`,
|
|
2763
|
+
);
|
|
2764
|
+
}
|
|
2765
|
+
return sql;
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
interface AccessPointSqlFixOptions {
|
|
2769
|
+
failedSql: string;
|
|
2770
|
+
question: string;
|
|
2771
|
+
errMsg: string;
|
|
2772
|
+
selectedAPs: TDSServiceSchema[];
|
|
2773
|
+
dataProductCoordinates: LegendAIOrchestratorDataProductCoordinates;
|
|
2774
|
+
modelContextEnrichment?: string;
|
|
2775
|
+
startTime: number;
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
async function retryAccessPointSqlFix(
|
|
2779
|
+
options: AccessPointSqlFixOptions,
|
|
2780
|
+
context: LegendAIOperationContext,
|
|
2781
|
+
metadata: LegendAIProductMetadata,
|
|
2782
|
+
): Promise<boolean> {
|
|
2783
|
+
const {
|
|
2784
|
+
failedSql,
|
|
2785
|
+
question,
|
|
2786
|
+
errMsg,
|
|
2787
|
+
selectedAPs,
|
|
2788
|
+
dataProductCoordinates,
|
|
2789
|
+
modelContextEnrichment,
|
|
2790
|
+
startTime,
|
|
2791
|
+
} = options;
|
|
2792
|
+
const { plugin, config, setMessages } = context;
|
|
2793
|
+
addThinkingStep(
|
|
2794
|
+
setMessages,
|
|
2795
|
+
'SQL error detected — asking LLM to fix the query on the same access points...',
|
|
2796
|
+
);
|
|
2797
|
+
const fixPrompt = plugin.buildAccessPointErrorCorrectionPrompt(
|
|
2798
|
+
failedSql,
|
|
2799
|
+
question,
|
|
2800
|
+
errMsg.slice(0, MAX_ERROR_MESSAGE_LENGTH),
|
|
2801
|
+
selectedAPs,
|
|
2802
|
+
context.history,
|
|
2803
|
+
modelContextEnrichment,
|
|
2804
|
+
);
|
|
2805
|
+
try {
|
|
2806
|
+
const fixResponse = await plugin.callLLM(fixPrompt, config);
|
|
2807
|
+
const fixResult = plugin.extractJudgeResult(fixResponse);
|
|
2808
|
+
const correctedSql = fixResult.correctedSql;
|
|
2809
|
+
if (correctedSql) {
|
|
2810
|
+
addThinkingStep(setMessages, 'Re-executing corrected SQL...');
|
|
2811
|
+
updateLastAssistant(setMessages, () => ({
|
|
2812
|
+
sql: correctedSql,
|
|
2813
|
+
isExecuting: true,
|
|
2814
|
+
}));
|
|
2815
|
+
const retryResult = await executeSqlForServices(
|
|
2816
|
+
correctedSql,
|
|
2817
|
+
selectedAPs,
|
|
2818
|
+
dataProductCoordinates,
|
|
2819
|
+
plugin,
|
|
2820
|
+
config,
|
|
2821
|
+
);
|
|
2822
|
+
await reportQueryResults(
|
|
2823
|
+
{
|
|
2824
|
+
currentSql: correctedSql,
|
|
2825
|
+
sqlResult: retryResult,
|
|
2826
|
+
question,
|
|
2827
|
+
services: selectedAPs,
|
|
2828
|
+
},
|
|
2829
|
+
metadata,
|
|
2830
|
+
context,
|
|
2831
|
+
startTime,
|
|
2832
|
+
false,
|
|
2833
|
+
);
|
|
2834
|
+
return true;
|
|
2835
|
+
}
|
|
2836
|
+
} catch (retryError) {
|
|
2837
|
+
assertErrorThrown(retryError);
|
|
2838
|
+
addThinkingStep(
|
|
2839
|
+
setMessages,
|
|
2840
|
+
`SQL correction also failed: ${retryError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
2841
|
+
);
|
|
2842
|
+
}
|
|
2843
|
+
return false;
|
|
2844
|
+
}
|
|
2845
|
+
|
|
1955
2846
|
/**
|
|
1956
2847
|
* Dedicated query flow for data product access points.
|
|
1957
2848
|
* Unlike processDataQuery this:
|
|
@@ -1965,9 +2856,9 @@ async function processAccessPointQuery(
|
|
|
1965
2856
|
context: LegendAIOperationContext,
|
|
1966
2857
|
startTime: number,
|
|
1967
2858
|
dataProductCoordinates: LegendAIOrchestratorDataProductCoordinates,
|
|
2859
|
+
modelContextEnrichment?: string,
|
|
1968
2860
|
): Promise<void> {
|
|
1969
2861
|
const { config, plugin, setMessages } = context;
|
|
1970
|
-
const hasOrchestratorFallback = Boolean(config.orchestratorUrl);
|
|
1971
2862
|
|
|
1972
2863
|
addThinkingStep(setMessages, 'Found relevant access points to query');
|
|
1973
2864
|
|
|
@@ -1978,6 +2869,7 @@ async function processAccessPointQuery(
|
|
|
1978
2869
|
selectedAPs,
|
|
1979
2870
|
context,
|
|
1980
2871
|
startTime,
|
|
2872
|
+
modelContextEnrichment,
|
|
1981
2873
|
);
|
|
1982
2874
|
|
|
1983
2875
|
if (!judgedSql) {
|
|
@@ -1988,7 +2880,7 @@ async function processAccessPointQuery(
|
|
|
1988
2880
|
handleSqlGenerationFailure(
|
|
1989
2881
|
setMessages,
|
|
1990
2882
|
startTime,
|
|
1991
|
-
|
|
2883
|
+
false,
|
|
1992
2884
|
SQL_GENERATION_FAILURE_WITH_ORCHESTRATOR,
|
|
1993
2885
|
SQL_GENERATION_FAILURE_NO_ORCHESTRATOR,
|
|
1994
2886
|
LegendAIErrorType.GENERATION,
|
|
@@ -1999,24 +2891,34 @@ async function processAccessPointQuery(
|
|
|
1999
2891
|
|
|
2000
2892
|
const sqlGenTimeValue = elapsedSeconds(startTime, 2);
|
|
2001
2893
|
completeThinkingSteps(setMessages);
|
|
2894
|
+
|
|
2895
|
+
const finalSql = await resolveNestedPCalls(
|
|
2896
|
+
judgedSql,
|
|
2897
|
+
question,
|
|
2898
|
+
selectedAPs,
|
|
2899
|
+
context,
|
|
2900
|
+
modelContextEnrichment,
|
|
2901
|
+
);
|
|
2902
|
+
|
|
2002
2903
|
updateLastAssistant(setMessages, () => ({
|
|
2003
|
-
sql:
|
|
2904
|
+
sql: finalSql,
|
|
2004
2905
|
sqlGenTime: sqlGenTimeValue,
|
|
2005
2906
|
isExecuting: true,
|
|
2006
2907
|
}));
|
|
2007
2908
|
|
|
2008
2909
|
const execStartTime = Date.now();
|
|
2009
2910
|
try {
|
|
2010
|
-
const
|
|
2011
|
-
|
|
2012
|
-
|
|
2911
|
+
const rawResult = await executeSqlForServices(
|
|
2912
|
+
finalSql,
|
|
2913
|
+
selectedAPs,
|
|
2013
2914
|
dataProductCoordinates,
|
|
2915
|
+
plugin,
|
|
2014
2916
|
config,
|
|
2015
2917
|
);
|
|
2016
2918
|
|
|
2017
2919
|
await reportQueryResults(
|
|
2018
2920
|
{
|
|
2019
|
-
currentSql:
|
|
2921
|
+
currentSql: finalSql,
|
|
2020
2922
|
sqlResult: rawResult,
|
|
2021
2923
|
question,
|
|
2022
2924
|
services: selectedAPs,
|
|
@@ -2024,35 +2926,60 @@ async function processAccessPointQuery(
|
|
|
2024
2926
|
metadata,
|
|
2025
2927
|
context,
|
|
2026
2928
|
startTime,
|
|
2027
|
-
|
|
2929
|
+
false,
|
|
2028
2930
|
);
|
|
2029
2931
|
} catch (executeError) {
|
|
2030
2932
|
assertErrorThrown(executeError);
|
|
2031
2933
|
const execErrorType = classifyError(executeError);
|
|
2934
|
+
const errMsg = executeError.message;
|
|
2935
|
+
const errorCategory = categorizeExecutionError(errMsg, executeError);
|
|
2936
|
+
|
|
2032
2937
|
addThinkingStep(
|
|
2033
2938
|
setMessages,
|
|
2034
|
-
`Execution failed: ${
|
|
2939
|
+
`Execution failed: ${errMsg.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
2035
2940
|
);
|
|
2941
|
+
|
|
2942
|
+
if (errorCategory === ExecutionErrorCategory.SQL_FIXABLE) {
|
|
2943
|
+
const retried = await retryAccessPointSqlFix(
|
|
2944
|
+
{
|
|
2945
|
+
failedSql: finalSql,
|
|
2946
|
+
question,
|
|
2947
|
+
errMsg,
|
|
2948
|
+
selectedAPs,
|
|
2949
|
+
dataProductCoordinates,
|
|
2950
|
+
...(modelContextEnrichment === undefined
|
|
2951
|
+
? {}
|
|
2952
|
+
: { modelContextEnrichment }),
|
|
2953
|
+
startTime,
|
|
2954
|
+
},
|
|
2955
|
+
context,
|
|
2956
|
+
metadata,
|
|
2957
|
+
);
|
|
2958
|
+
if (retried) {
|
|
2959
|
+
return;
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2036
2963
|
finishWithThinkingError(
|
|
2037
2964
|
setMessages,
|
|
2038
|
-
buildExecutionErrorMessage(
|
|
2965
|
+
buildExecutionErrorMessage(errMsg, selectedAPs),
|
|
2039
2966
|
startTime,
|
|
2040
2967
|
execErrorType === LegendAIErrorType.GENERAL
|
|
2041
2968
|
? LegendAIErrorType.EXECUTION
|
|
2042
2969
|
: execErrorType,
|
|
2043
2970
|
);
|
|
2971
|
+
const queriedGroups = [
|
|
2972
|
+
...new Set(
|
|
2973
|
+
selectedAPs
|
|
2974
|
+
.map((ap) => ap.accessPointGroupTitle)
|
|
2975
|
+
.filter((t): t is string => t !== undefined),
|
|
2976
|
+
),
|
|
2977
|
+
];
|
|
2044
2978
|
updateLastAssistant(setMessages, () => ({
|
|
2045
2979
|
execTime: elapsedSeconds(execStartTime, 2),
|
|
2046
2980
|
isExecuting: false,
|
|
2047
2981
|
suggestedQueries: buildFallbackSuggestions(selectedAPs),
|
|
2048
|
-
|
|
2049
|
-
? {
|
|
2050
|
-
fallbackAction: {
|
|
2051
|
-
label: ORCHESTRATOR_FALLBACK_LABEL,
|
|
2052
|
-
actionId: LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
|
|
2053
|
-
},
|
|
2054
|
-
}
|
|
2055
|
-
: {}),
|
|
2982
|
+
queriedAccessPointGroups: queriedGroups,
|
|
2056
2983
|
}));
|
|
2057
2984
|
}
|
|
2058
2985
|
}
|
|
@@ -2065,6 +2992,7 @@ async function processDataQuery(
|
|
|
2065
2992
|
context: LegendAIOperationContext,
|
|
2066
2993
|
startTime: number,
|
|
2067
2994
|
orchestratorOptions?: LegendAIOrchestratorOptionsParam,
|
|
2995
|
+
modelContextEnrichment?: string,
|
|
2068
2996
|
): Promise<void> {
|
|
2069
2997
|
const { config, setMessages } = context;
|
|
2070
2998
|
const dataProductCoordinates = orchestratorOptions?.dataProductCoordinates;
|
|
@@ -2099,6 +3027,7 @@ async function processDataQuery(
|
|
|
2099
3027
|
context,
|
|
2100
3028
|
startTime,
|
|
2101
3029
|
metadata,
|
|
3030
|
+
modelContextEnrichment,
|
|
2102
3031
|
);
|
|
2103
3032
|
|
|
2104
3033
|
if (!judgedSql) {
|
|
@@ -2242,6 +3171,7 @@ async function routeToAccessPointOrData(
|
|
|
2242
3171
|
context: LegendAIOperationContext,
|
|
2243
3172
|
startTime: number,
|
|
2244
3173
|
orchestratorOpts: LegendAIOrchestratorOptionsParam | undefined,
|
|
3174
|
+
modelContextEnrichment?: string,
|
|
2245
3175
|
): Promise<void> {
|
|
2246
3176
|
const dataProductCoordinates = orchestratorOpts?.dataProductCoordinates;
|
|
2247
3177
|
const { tdsServices, accessPoints } = splitServicesByType(services);
|
|
@@ -2257,6 +3187,7 @@ async function routeToAccessPointOrData(
|
|
|
2257
3187
|
context,
|
|
2258
3188
|
startTime,
|
|
2259
3189
|
dataProductCoordinates,
|
|
3190
|
+
modelContextEnrichment,
|
|
2260
3191
|
);
|
|
2261
3192
|
} else {
|
|
2262
3193
|
await processDataQuery(
|
|
@@ -2267,59 +3198,11 @@ async function routeToAccessPointOrData(
|
|
|
2267
3198
|
context,
|
|
2268
3199
|
startTime,
|
|
2269
3200
|
orchestratorOpts,
|
|
3201
|
+
modelContextEnrichment,
|
|
2270
3202
|
);
|
|
2271
3203
|
}
|
|
2272
3204
|
}
|
|
2273
3205
|
|
|
2274
|
-
async function handleAmbiguousIntent(
|
|
2275
|
-
question: string,
|
|
2276
|
-
services: TDSServiceSchema[],
|
|
2277
|
-
coordinates: string,
|
|
2278
|
-
metadata: LegendAIProductMetadata,
|
|
2279
|
-
context: LegendAIOperationContext,
|
|
2280
|
-
startTime: number,
|
|
2281
|
-
orchestratorOpts: LegendAIOrchestratorOptionsParam | undefined,
|
|
2282
|
-
): Promise<void> {
|
|
2283
|
-
const { setMessages } = context;
|
|
2284
|
-
addThinkingStep(
|
|
2285
|
-
setMessages,
|
|
2286
|
-
'Intent is ambiguous, providing metadata context and querying data...',
|
|
2287
|
-
);
|
|
2288
|
-
let metadataOverview: string | undefined;
|
|
2289
|
-
try {
|
|
2290
|
-
addThinkingStep(setMessages, 'Building metadata context...');
|
|
2291
|
-
metadataOverview = await buildMetadataOverview(question, metadata, context);
|
|
2292
|
-
} catch (metadataError) {
|
|
2293
|
-
assertErrorThrown(metadataError);
|
|
2294
|
-
addThinkingStep(
|
|
2295
|
-
setMessages,
|
|
2296
|
-
`Metadata context failed: ${metadataError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
2297
|
-
);
|
|
2298
|
-
}
|
|
2299
|
-
|
|
2300
|
-
try {
|
|
2301
|
-
await routeToAccessPointOrData(
|
|
2302
|
-
question,
|
|
2303
|
-
services,
|
|
2304
|
-
coordinates,
|
|
2305
|
-
metadata,
|
|
2306
|
-
context,
|
|
2307
|
-
startTime,
|
|
2308
|
-
orchestratorOpts,
|
|
2309
|
-
);
|
|
2310
|
-
if (metadataOverview) {
|
|
2311
|
-
attachMetadataOverview(setMessages, metadataOverview);
|
|
2312
|
-
}
|
|
2313
|
-
} catch (queryError) {
|
|
2314
|
-
assertErrorThrown(queryError);
|
|
2315
|
-
addThinkingStep(
|
|
2316
|
-
setMessages,
|
|
2317
|
-
'Query failed, answering from product metadata...',
|
|
2318
|
-
);
|
|
2319
|
-
await handleMetadataQuestion(question, metadata, context, startTime, true);
|
|
2320
|
-
}
|
|
2321
|
-
}
|
|
2322
|
-
|
|
2323
3206
|
export async function processQuestion(
|
|
2324
3207
|
question: string,
|
|
2325
3208
|
services: TDSServiceSchema[],
|
|
@@ -2328,9 +3211,13 @@ export async function processQuestion(
|
|
|
2328
3211
|
context: LegendAIOperationContext,
|
|
2329
3212
|
dataProductCoordinates?: LegendAIOrchestratorDataProductCoordinates,
|
|
2330
3213
|
pureExecutionContext?: QueryExplicitExecutionContextInfo,
|
|
3214
|
+
modelContext?: LegendAIModelContext,
|
|
2331
3215
|
): Promise<void> {
|
|
2332
3216
|
const { config, plugin, setMessages } = context;
|
|
2333
3217
|
const startTime = Date.now();
|
|
3218
|
+
const modelContextEnrichment = modelContext
|
|
3219
|
+
? buildModelContextEnrichmentText(modelContext, services)
|
|
3220
|
+
: undefined;
|
|
2334
3221
|
|
|
2335
3222
|
try {
|
|
2336
3223
|
addThinkingStep(setMessages, 'Analyzing your question...');
|
|
@@ -2351,6 +3238,8 @@ export async function processQuestion(
|
|
|
2351
3238
|
context,
|
|
2352
3239
|
startTime,
|
|
2353
3240
|
false,
|
|
3241
|
+
services,
|
|
3242
|
+
modelContextEnrichment,
|
|
2354
3243
|
);
|
|
2355
3244
|
if (config.orchestratorUrl && dataProductCoordinates) {
|
|
2356
3245
|
updateLastAssistant(setMessages, () => ({
|
|
@@ -2363,34 +3252,6 @@ export async function processQuestion(
|
|
|
2363
3252
|
return;
|
|
2364
3253
|
}
|
|
2365
3254
|
|
|
2366
|
-
const fastIntent = classifyQuestionIntentFast(question, true);
|
|
2367
|
-
if (
|
|
2368
|
-
fastIntent.intent === LegendAIQuestionIntent.METADATA &&
|
|
2369
|
-
!fastIntent.ambiguous
|
|
2370
|
-
) {
|
|
2371
|
-
await handleMetadataQuestion(
|
|
2372
|
-
question,
|
|
2373
|
-
metadata,
|
|
2374
|
-
context,
|
|
2375
|
-
startTime,
|
|
2376
|
-
true,
|
|
2377
|
-
);
|
|
2378
|
-
return;
|
|
2379
|
-
}
|
|
2380
|
-
|
|
2381
|
-
if (fastIntent.ambiguous) {
|
|
2382
|
-
await handleAmbiguousIntent(
|
|
2383
|
-
question,
|
|
2384
|
-
services,
|
|
2385
|
-
coordinates,
|
|
2386
|
-
metadata,
|
|
2387
|
-
context,
|
|
2388
|
-
startTime,
|
|
2389
|
-
orchestratorOpts,
|
|
2390
|
-
);
|
|
2391
|
-
return;
|
|
2392
|
-
}
|
|
2393
|
-
|
|
2394
3255
|
const serviceNames = services.map((s) => s.title);
|
|
2395
3256
|
const intent = await plugin.classifyQuestionIntent(
|
|
2396
3257
|
question,
|
|
@@ -2406,6 +3267,8 @@ export async function processQuestion(
|
|
|
2406
3267
|
context,
|
|
2407
3268
|
startTime,
|
|
2408
3269
|
true,
|
|
3270
|
+
services,
|
|
3271
|
+
modelContextEnrichment,
|
|
2409
3272
|
);
|
|
2410
3273
|
return;
|
|
2411
3274
|
}
|
|
@@ -2419,12 +3282,13 @@ export async function processQuestion(
|
|
|
2419
3282
|
context,
|
|
2420
3283
|
startTime,
|
|
2421
3284
|
orchestratorOpts,
|
|
3285
|
+
modelContextEnrichment,
|
|
2422
3286
|
);
|
|
2423
3287
|
} catch (sqlError) {
|
|
2424
3288
|
assertErrorThrown(sqlError);
|
|
2425
3289
|
addThinkingStep(
|
|
2426
3290
|
setMessages,
|
|
2427
|
-
'
|
|
3291
|
+
'Query failed, answering from product metadata...',
|
|
2428
3292
|
);
|
|
2429
3293
|
await handleMetadataQuestion(
|
|
2430
3294
|
question,
|
|
@@ -2432,6 +3296,8 @@ export async function processQuestion(
|
|
|
2432
3296
|
context,
|
|
2433
3297
|
startTime,
|
|
2434
3298
|
true,
|
|
3299
|
+
services,
|
|
3300
|
+
modelContextEnrichment,
|
|
2435
3301
|
);
|
|
2436
3302
|
appendFallbackSuggestions(setMessages, services);
|
|
2437
3303
|
}
|
|
@@ -2458,6 +3324,7 @@ async function routeDataQueryWithErrorHandling(
|
|
|
2458
3324
|
metadata: LegendAIProductMetadata,
|
|
2459
3325
|
context: LegendAIOperationContext,
|
|
2460
3326
|
orchestratorOptions: LegendAIOrchestratorOptionsParam | undefined,
|
|
3327
|
+
modelContextEnrichment?: string,
|
|
2461
3328
|
): Promise<void> {
|
|
2462
3329
|
const { setMessages } = context;
|
|
2463
3330
|
const startTime = Date.now();
|
|
@@ -2471,6 +3338,7 @@ async function routeDataQueryWithErrorHandling(
|
|
|
2471
3338
|
context,
|
|
2472
3339
|
startTime,
|
|
2473
3340
|
orchestratorOptions,
|
|
3341
|
+
modelContextEnrichment,
|
|
2474
3342
|
);
|
|
2475
3343
|
} catch (error) {
|
|
2476
3344
|
assertErrorThrown(error);
|
|
@@ -2495,9 +3363,13 @@ export async function processQuestionWithIntent(
|
|
|
2495
3363
|
metadata: LegendAIProductMetadata,
|
|
2496
3364
|
context: LegendAIOperationContext,
|
|
2497
3365
|
orchestratorOptions?: LegendAIOrchestratorOptionsParam,
|
|
3366
|
+
modelContext?: LegendAIModelContext,
|
|
2498
3367
|
): Promise<void> {
|
|
2499
3368
|
const { config, setMessages } = context;
|
|
2500
3369
|
const dataProductCoordinates = orchestratorOptions?.dataProductCoordinates;
|
|
3370
|
+
const modelContextEnrichment = modelContext
|
|
3371
|
+
? buildModelContextEnrichmentText(modelContext, services)
|
|
3372
|
+
: undefined;
|
|
2501
3373
|
|
|
2502
3374
|
if (intent === LegendAIQuestionIntent.METADATA) {
|
|
2503
3375
|
const startTime = Date.now();
|
|
@@ -2508,6 +3380,8 @@ export async function processQuestionWithIntent(
|
|
|
2508
3380
|
context,
|
|
2509
3381
|
startTime,
|
|
2510
3382
|
services.length > 0,
|
|
3383
|
+
services,
|
|
3384
|
+
modelContextEnrichment,
|
|
2511
3385
|
);
|
|
2512
3386
|
} catch (error) {
|
|
2513
3387
|
assertErrorThrown(error);
|
|
@@ -2538,11 +3412,20 @@ export async function processQuestionWithIntent(
|
|
|
2538
3412
|
metadata,
|
|
2539
3413
|
context,
|
|
2540
3414
|
orchestratorOptions,
|
|
3415
|
+
modelContextEnrichment,
|
|
2541
3416
|
);
|
|
2542
3417
|
return;
|
|
2543
3418
|
}
|
|
2544
3419
|
const startTime = Date.now();
|
|
2545
|
-
await handleMetadataQuestion(
|
|
3420
|
+
await handleMetadataQuestion(
|
|
3421
|
+
question,
|
|
3422
|
+
metadata,
|
|
3423
|
+
context,
|
|
3424
|
+
startTime,
|
|
3425
|
+
false,
|
|
3426
|
+
services,
|
|
3427
|
+
modelContextEnrichment,
|
|
3428
|
+
);
|
|
2546
3429
|
updateLastAssistant(setMessages, () => ({
|
|
2547
3430
|
fallbackAction: {
|
|
2548
3431
|
label: ORCHESTRATOR_FALLBACK_LABEL,
|
|
@@ -2559,5 +3442,6 @@ export async function processQuestionWithIntent(
|
|
|
2559
3442
|
metadata,
|
|
2560
3443
|
context,
|
|
2561
3444
|
orchestratorOptions,
|
|
3445
|
+
modelContextEnrichment,
|
|
2562
3446
|
);
|
|
2563
3447
|
}
|