@finos/legend-lego 2.0.211 → 2.0.213
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 +12 -12
- package/lib/legend-ai/LegendAIDocEnrichment.d.ts.map +1 -1
- package/lib/legend-ai/LegendAIDocEnrichment.js +317 -86
- package/lib/legend-ai/LegendAIDocEnrichment.js.map +1 -1
- package/lib/legend-ai/LegendAIHostIntegration.d.ts +31 -0
- package/lib/legend-ai/LegendAIHostIntegration.d.ts.map +1 -0
- package/lib/legend-ai/LegendAIHostIntegration.js +50 -0
- package/lib/legend-ai/LegendAIHostIntegration.js.map +1 -0
- package/lib/legend-ai/LegendAIServiceRetrieval.d.ts.map +1 -1
- package/lib/legend-ai/LegendAIServiceRetrieval.js +34 -35
- package/lib/legend-ai/LegendAIServiceRetrieval.js.map +1 -1
- package/lib/legend-ai/LegendAITypes.d.ts +85 -7
- package/lib/legend-ai/LegendAITypes.d.ts.map +1 -1
- package/lib/legend-ai/LegendAITypes.js +99 -1
- package/lib/legend-ai/LegendAITypes.js.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts +36 -3
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js +29 -0
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js.map +1 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.d.ts.map +1 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.js +7 -0
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.js.map +1 -1
- package/lib/legend-ai/components/LegendAICharts.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAICharts.js +6 -10
- 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 +263 -41
- package/lib/legend-ai/components/LegendAIChat.js.map +1 -1
- package/lib/legend-ai/components/LegendAIChatInput.d.ts +1 -1
- package/lib/legend-ai/components/LegendAIChatInput.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIChatInput.js +2 -1
- package/lib/legend-ai/components/LegendAIChatInput.js.map +1 -1
- package/lib/legend-ai/index.d.ts +5 -2
- package/lib/legend-ai/index.d.ts.map +1 -1
- package/lib/legend-ai/index.js +5 -2
- package/lib/legend-ai/index.js.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts +9 -3
- package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatProcessors.js +794 -220
- package/lib/legend-ai/stores/LegendAIChatProcessors.js.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatState.d.ts.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatState.js +47 -21
- package/lib/legend-ai/stores/LegendAIChatState.js.map +1 -1
- package/lib/legend-ai/stores/LegendAIJoinAnalysis.d.ts +29 -0
- package/lib/legend-ai/stores/LegendAIJoinAnalysis.d.ts.map +1 -0
- package/lib/legend-ai/stores/LegendAIJoinAnalysis.js +124 -0
- package/lib/legend-ai/stores/LegendAIJoinAnalysis.js.map +1 -0
- package/lib/legend-ai/stores/LegendAISqlHelpers.d.ts +25 -0
- package/lib/legend-ai/stores/LegendAISqlHelpers.d.ts.map +1 -0
- package/lib/legend-ai/stores/LegendAISqlHelpers.js +64 -0
- package/lib/legend-ai/stores/LegendAISqlHelpers.js.map +1 -0
- package/lib/legend-ai/stores/LegendAISqlJoinSanitizers.d.ts +22 -0
- package/lib/legend-ai/stores/LegendAISqlJoinSanitizers.d.ts.map +1 -0
- package/lib/legend-ai/stores/LegendAISqlJoinSanitizers.js +411 -0
- package/lib/legend-ai/stores/LegendAISqlJoinSanitizers.js.map +1 -0
- package/package.json +6 -6
- package/src/legend-ai/LegendAIDocEnrichment.ts +435 -97
- package/src/legend-ai/LegendAIHostIntegration.tsx +80 -0
- package/src/legend-ai/LegendAIServiceRetrieval.ts +50 -35
- package/src/legend-ai/LegendAITypes.ts +169 -5
- package/src/legend-ai/LegendAI_LegendApplicationPlugin_Extension.ts +73 -1
- package/src/legend-ai/__test-utils__/LegendAITestUtils.ts +10 -0
- package/src/legend-ai/components/LegendAICharts.tsx +30 -33
- package/src/legend-ai/components/LegendAIChat.tsx +555 -91
- package/src/legend-ai/components/LegendAIChatInput.tsx +6 -2
- package/src/legend-ai/index.ts +22 -3
- package/src/legend-ai/stores/LegendAIChatProcessors.ts +1286 -322
- package/src/legend-ai/stores/LegendAIChatState.ts +73 -28
- package/src/legend-ai/stores/LegendAIJoinAnalysis.ts +165 -0
- package/src/legend-ai/stores/LegendAISqlHelpers.ts +82 -0
- package/src/legend-ai/stores/LegendAISqlJoinSanitizers.ts +515 -0
- package/tsconfig.json +4 -0
|
@@ -15,7 +15,12 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type React from 'react';
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
assertErrorThrown,
|
|
20
|
+
guaranteeNonNullable,
|
|
21
|
+
isNonNullable,
|
|
22
|
+
uuid,
|
|
23
|
+
} from '@finos/legend-shared';
|
|
19
24
|
import {
|
|
20
25
|
type TDSServiceSchema,
|
|
21
26
|
type LegendAIConfig,
|
|
@@ -25,16 +30,20 @@ import {
|
|
|
25
30
|
type LegendAIConversationTurn,
|
|
26
31
|
type LegendAIProductMetadata,
|
|
27
32
|
type LegendAIModelContext,
|
|
33
|
+
LegendAISelfHealKind,
|
|
28
34
|
LegendAIQuestionIntent,
|
|
29
35
|
LegendAIResponseOutcome,
|
|
30
36
|
LegendAIThinkingStepStatus,
|
|
31
37
|
LegendAIMessageRole,
|
|
32
38
|
LegendAIErrorType,
|
|
39
|
+
LegendAIExecutionTimeoutError,
|
|
33
40
|
LegendAIServiceError,
|
|
34
41
|
LegendAIUnsupportedEngineShapeError,
|
|
35
42
|
TDSServiceSourceType,
|
|
36
43
|
buildColumnDefsFromNames,
|
|
37
44
|
LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
|
|
45
|
+
LEGEND_AI_ALTERNATE_ROOT_ACTION_ID,
|
|
46
|
+
LEGEND_AI_FEEDBACK_PROMPT,
|
|
38
47
|
getTodayISO,
|
|
39
48
|
} from '../LegendAITypes.js';
|
|
40
49
|
import {
|
|
@@ -46,12 +55,19 @@ import {
|
|
|
46
55
|
} from '../LegendAI_LegendApplicationPlugin_Extension.js';
|
|
47
56
|
import {
|
|
48
57
|
type QueryExplicitExecutionContextInfo,
|
|
58
|
+
type TDSRowDataType,
|
|
49
59
|
extractElementNameFromPath,
|
|
50
60
|
} from '@finos/legend-graph';
|
|
51
61
|
import {
|
|
52
62
|
buildEnrichedBusinessContext,
|
|
53
63
|
buildModelContextEnrichmentText,
|
|
64
|
+
buildModelCatalogText,
|
|
65
|
+
buildDataQueryApproachText,
|
|
54
66
|
findBestAlternateRoot,
|
|
67
|
+
rankEntities,
|
|
68
|
+
relaxExactStringFilters,
|
|
69
|
+
extractFilteredColumns,
|
|
70
|
+
buildProbedValueHints,
|
|
55
71
|
splitIdentifierTokens,
|
|
56
72
|
tokenizeText,
|
|
57
73
|
} from '../LegendAIDocEnrichment.js';
|
|
@@ -63,30 +79,49 @@ import {
|
|
|
63
79
|
isNumericColumn,
|
|
64
80
|
isStringColumn,
|
|
65
81
|
} from '../components/LegendAIChatHelpers.js';
|
|
82
|
+
import {
|
|
83
|
+
HAS_LIMIT_PATTERN,
|
|
84
|
+
AP_CALL_PATTERN,
|
|
85
|
+
servicePId,
|
|
86
|
+
pureRelationColumnRef,
|
|
87
|
+
} from './LegendAISqlHelpers.js';
|
|
88
|
+
import {
|
|
89
|
+
buildCrossJoinZeroRowExplanation,
|
|
90
|
+
buildJoinablePairSuggestions,
|
|
91
|
+
} from './LegendAIJoinAnalysis.js';
|
|
92
|
+
import {
|
|
93
|
+
boundCrossAccessPointJoinDrivingSide,
|
|
94
|
+
sanitizeJoinDuplicateColumns,
|
|
95
|
+
sanitizeJoinOrderBy,
|
|
96
|
+
sanitizeJoinSameKeyColumns,
|
|
97
|
+
wrapBareJoinAccessPoints,
|
|
98
|
+
} from './LegendAISqlJoinSanitizers.js';
|
|
66
99
|
|
|
67
100
|
const MAX_ERROR_MESSAGE_LENGTH = 500;
|
|
101
|
+
const MAX_DISPLAYED_ERROR_LENGTH = 4000;
|
|
68
102
|
const MAX_THINKING_ERROR_PREVIEW_LENGTH = 200;
|
|
69
103
|
const DEFAULT_MAX_JUDGE_ATTEMPTS = 5;
|
|
70
104
|
const DEFAULT_MAX_EXECUTION_RETRIES = 3;
|
|
71
105
|
const ANALYSIS_TIMEOUT_MS = 15_000;
|
|
72
106
|
const ORCHESTRATOR_GENERATION_TIMEOUT_MS = 120_000;
|
|
107
|
+
const DISTINCT_PROBE_ROW_LIMIT = 5000;
|
|
108
|
+
const MAX_RECOVERED_FILTER_COLUMNS = 4;
|
|
73
109
|
const EXECUTION_TIMEOUT_MS = 300_000;
|
|
74
110
|
const ANALYSIS_PREVIEW_ROW_LIMIT = 3;
|
|
75
111
|
const ANALYSIS_PREVIEW_VALUE_LIMIT = 40;
|
|
112
|
+
const MAX_ANALYSIS_ROWS = 100;
|
|
76
113
|
const MAX_NON_SQL_PASS_ATTEMPTS = 2;
|
|
77
|
-
const JOIN_PATTERN = /\bJOIN\b/i;
|
|
78
|
-
const ORDER_BY_SPLIT = /\bORDER\s+BY\b/i;
|
|
79
114
|
const UNION_ALL_PATTERN = /\bUNION\s+ALL\b/i;
|
|
80
115
|
const LITERAL_COL_PATTERN = /,\s*'[^']*'\s+AS\s+(?:"[^"]+"|[a-z]\w*)/gi;
|
|
81
|
-
const SELECT_ALIAS_PATTERN =
|
|
82
|
-
/\b(?<tbl>[a-z]\w*)\s*\.\s*"(?<col>[^"]+)"\s+AS\s+(?:"(?<qAlias>[^"]+)"|(?<uAlias>\w+))/gi;
|
|
83
|
-
const ALIAS_DOT_COL_PATTERN = /\b(?<tbl>[a-z]\w*)\s*\.\s*"(?<col>[^"]+)"/gi;
|
|
84
116
|
const SERVICE_CALL_PATTERN = /\bservice\s*\([^()]*\)/gi;
|
|
85
117
|
const DEFAULT_SAFETY_LIMIT = 1000;
|
|
86
|
-
const HAS_LIMIT_PATTERN = /\bLIMIT\s+\d+/i;
|
|
87
118
|
const HAS_AGGREGATION_PATTERN =
|
|
88
|
-
/\bGROUP\s+BY\b|\
|
|
119
|
+
/\bGROUP\s+BY\b|\b(?:COUNT|SUM|AVG|MIN|MAX)\s*\(|\bSELECT\s+DISTINCT\b|\bHAVING\b|\bQUALIFY\b/i;
|
|
89
120
|
const MAX_SERVICES_FOR_LLM_SELECTION = 30;
|
|
121
|
+
const VALUE_GROUNDING_TIME_BUDGET_MS = 4000;
|
|
122
|
+
const AP_SQL_EXECUTION_TIMEOUT_MS = 90_000;
|
|
123
|
+
const AP_SQL_EXECUTION_TIMEOUT_MESSAGE =
|
|
124
|
+
'This query is scanning very large access-point feeds and is taking too long to return. Narrow it with a date or entity filter, join on the full key (including the date), or use a *_LATEST snapshot access point.';
|
|
90
125
|
const ORCHESTRATOR_FALLBACK_LABEL = 'Try Legend AI Orchestrator';
|
|
91
126
|
const SQL_GENERATION_FAILURE_WITH_ORCHESTRATOR =
|
|
92
127
|
'SQL generation could not handle this query. You can try the Legend AI Orchestrator to generate a Pure query instead.';
|
|
@@ -99,6 +134,13 @@ const SERVICE_PARAM_DATE_LIKE_PATTERNS: readonly RegExp[] = [
|
|
|
99
134
|
];
|
|
100
135
|
const GENERIC_TABLE_PATTERNS = /combined|consolidated|all|master|summary/i;
|
|
101
136
|
|
|
137
|
+
const MIN_ALL_NULL_COLUMNS_FOR_JOIN_HINT = 2;
|
|
138
|
+
const JOIN_HINT_COLUMN_PREVIEW_LIMIT = 3;
|
|
139
|
+
|
|
140
|
+
const MAX_AGGREGATE_COLUMNS = 12;
|
|
141
|
+
const MAX_AGGREGATE_VALUES_PER_COLUMN = 8;
|
|
142
|
+
const MIN_NUMERIC_DISTINCT_RATIO = 0.5;
|
|
143
|
+
|
|
102
144
|
function isLikelySqlQuery(text: string): boolean {
|
|
103
145
|
const trimmed = text.trim().toLowerCase();
|
|
104
146
|
return (
|
|
@@ -167,6 +209,7 @@ export function createMessagePair(
|
|
|
167
209
|
suggestedQueries: [],
|
|
168
210
|
fallbackAction: null,
|
|
169
211
|
queriedAccessPointGroups: [],
|
|
212
|
+
queriedAccessPoints: [],
|
|
170
213
|
},
|
|
171
214
|
];
|
|
172
215
|
}
|
|
@@ -272,6 +315,9 @@ export function categorizeExecutionError(
|
|
|
272
315
|
errMsg: string,
|
|
273
316
|
error?: Error,
|
|
274
317
|
): ExecutionErrorCategory {
|
|
318
|
+
if (error instanceof LegendAIExecutionTimeoutError) {
|
|
319
|
+
return ExecutionErrorCategory.NONE;
|
|
320
|
+
}
|
|
275
321
|
if (error instanceof LegendAIUnsupportedEngineShapeError) {
|
|
276
322
|
return ExecutionErrorCategory.SQL_FIXABLE;
|
|
277
323
|
}
|
|
@@ -289,6 +335,15 @@ export function categorizeExecutionError(
|
|
|
289
335
|
return ExecutionErrorCategory.NONE;
|
|
290
336
|
}
|
|
291
337
|
|
|
338
|
+
const ACCESS_ERROR_PATTERN =
|
|
339
|
+
/insufficient privileges|access.*denied|permission.*denied|not authorized|unauthorized|403|entitlement/i;
|
|
340
|
+
|
|
341
|
+
// Whether an execution error message indicates an access/permission problem (as
|
|
342
|
+
// opposed to a SQL compilation or schema error), gating the "Request Access" UI.
|
|
343
|
+
export function looksLikeAccessError(errorMsg: string | undefined): boolean {
|
|
344
|
+
return errorMsg !== undefined && ACCESS_ERROR_PATTERN.test(errorMsg);
|
|
345
|
+
}
|
|
346
|
+
|
|
292
347
|
export function finishWithThinkingError(
|
|
293
348
|
setMessages: MessageSetter,
|
|
294
349
|
errorMsg: string,
|
|
@@ -301,13 +356,47 @@ export function finishWithThinkingError(
|
|
|
301
356
|
? { ...s, status: LegendAIThinkingStepStatus.ERROR }
|
|
302
357
|
: s,
|
|
303
358
|
),
|
|
304
|
-
error: errorMsg.slice(0,
|
|
359
|
+
error: errorMsg.slice(0, MAX_DISPLAYED_ERROR_LENGTH),
|
|
305
360
|
errorType: errorType ?? null,
|
|
306
361
|
isProcessing: false,
|
|
307
362
|
thinkingDuration: elapsedSeconds(startTime),
|
|
308
363
|
}));
|
|
309
364
|
}
|
|
310
365
|
|
|
366
|
+
export function filterHistoryForAccessPoints(
|
|
367
|
+
history: LegendAIConversationTurn[],
|
|
368
|
+
currentAccessPoints: ReadonlySet<string>,
|
|
369
|
+
): LegendAIConversationTurn[] {
|
|
370
|
+
if (currentAccessPoints.size === 0) {
|
|
371
|
+
return history;
|
|
372
|
+
}
|
|
373
|
+
return history.filter((turn) => {
|
|
374
|
+
if (!turn.queriedAccessPoints || turn.queriedAccessPoints.length === 0) {
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
return turn.queriedAccessPoints.some((ap) => currentAccessPoints.has(ap));
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Access-point names referenced by `p('...')` calls in the SQL, in
|
|
382
|
+
// first-appearance (driving-first) order and de-duplicated. Computed once at
|
|
383
|
+
// generation to persist on the message so downstream consumers (UI, history,
|
|
384
|
+
// integrations) resolve access points structurally instead of re-parsing SQL.
|
|
385
|
+
function orderedAccessPointsFromSql(sql: string): string[] {
|
|
386
|
+
const ordered: string[] = [];
|
|
387
|
+
const seen = new Set<string>();
|
|
388
|
+
for (const match of sql.matchAll(AP_CALL_PATTERN)) {
|
|
389
|
+
const path = match.groups?.pId ?? '';
|
|
390
|
+
const dotIdx = path.lastIndexOf('.');
|
|
391
|
+
const apName = dotIdx === -1 ? path : path.slice(dotIdx + 1);
|
|
392
|
+
if (apName.length > 0 && !seen.has(apName)) {
|
|
393
|
+
seen.add(apName);
|
|
394
|
+
ordered.push(apName);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return ordered;
|
|
398
|
+
}
|
|
399
|
+
|
|
311
400
|
function buildTurnFromAssistant(
|
|
312
401
|
userText: string,
|
|
313
402
|
asstMsg: LegendAIAssistantMessage,
|
|
@@ -318,6 +407,9 @@ function buildTurnFromAssistant(
|
|
|
318
407
|
sql: asstMsg.sql,
|
|
319
408
|
intent: LegendAIQuestionIntent.DATA_QUERY,
|
|
320
409
|
};
|
|
410
|
+
if (asstMsg.queriedAccessPoints.length > 0) {
|
|
411
|
+
turn.queriedAccessPoints = asstMsg.queriedAccessPoints;
|
|
412
|
+
}
|
|
321
413
|
if (asstMsg.error) {
|
|
322
414
|
turn.resultSummary = `ERROR: ${asstMsg.error.slice(0, 200)}`;
|
|
323
415
|
} else if (asstMsg.gridData) {
|
|
@@ -530,10 +622,10 @@ export function buildExecutionErrorMessage(
|
|
|
530
622
|
return errParts.join('');
|
|
531
623
|
}
|
|
532
624
|
|
|
533
|
-
if (errLower.includes('
|
|
625
|
+
if (errLower.includes('duplicate column')) {
|
|
534
626
|
return [
|
|
535
|
-
'
|
|
536
|
-
'\
|
|
627
|
+
'This join produced two output columns with the same name, which the execution engine cannot return.',
|
|
628
|
+
'\nGive each selected column a unique alias (e.g. `<column> AS <accessPoint>_<column>`) so the joined result stays distinct.',
|
|
537
629
|
].join('');
|
|
538
630
|
}
|
|
539
631
|
|
|
@@ -680,7 +772,7 @@ function formatPreviewValue(value: unknown): string {
|
|
|
680
772
|
function buildJoinDiagnosticNote(
|
|
681
773
|
query: string,
|
|
682
774
|
columns: string[],
|
|
683
|
-
rows:
|
|
775
|
+
rows: TDSRowDataType[],
|
|
684
776
|
): string {
|
|
685
777
|
if (!/\bJOIN\b/i.test(query) || rows.length === 0 || columns.length < 2) {
|
|
686
778
|
return '';
|
|
@@ -690,11 +782,8 @@ function buildJoinDiagnosticNote(
|
|
|
690
782
|
const someDataColumns: string[] = [];
|
|
691
783
|
for (const col of columns) {
|
|
692
784
|
const allNull = rows.every((row) => {
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
return val === null || val === undefined;
|
|
696
|
-
}
|
|
697
|
-
return true;
|
|
785
|
+
const val = row[col];
|
|
786
|
+
return val === null || val === undefined;
|
|
698
787
|
});
|
|
699
788
|
if (allNull) {
|
|
700
789
|
allNullColumns.push(col);
|
|
@@ -706,10 +795,10 @@ function buildJoinDiagnosticNote(
|
|
|
706
795
|
if (
|
|
707
796
|
allNullColumns.length > 0 &&
|
|
708
797
|
someDataColumns.length > 0 &&
|
|
709
|
-
allNullColumns.length >=
|
|
798
|
+
allNullColumns.length >= MIN_ALL_NULL_COLUMNS_FOR_JOIN_HINT
|
|
710
799
|
) {
|
|
711
800
|
return (
|
|
712
|
-
`Note: ${allNullColumns.length} columns (${allNullColumns.slice(0,
|
|
801
|
+
`Note: ${allNullColumns.length} columns (${allNullColumns.slice(0, JOIN_HINT_COLUMN_PREVIEW_LIMIT).join(', ')}${allNullColumns.length > JOIN_HINT_COLUMN_PREVIEW_LIMIT ? ', ...' : ''}) ` +
|
|
713
802
|
'returned entirely NULL values. This typically means the joined services do not share ' +
|
|
714
803
|
'overlapping values for the join key. The services may track different records. ' +
|
|
715
804
|
'Try querying each service separately to explore their data independently.'
|
|
@@ -719,25 +808,101 @@ function buildJoinDiagnosticNote(
|
|
|
719
808
|
return '';
|
|
720
809
|
}
|
|
721
810
|
|
|
811
|
+
interface ColumnDistribution {
|
|
812
|
+
column: string;
|
|
813
|
+
counts: Map<string, number>;
|
|
814
|
+
present: number;
|
|
815
|
+
numeric: number;
|
|
816
|
+
min: number;
|
|
817
|
+
max: number;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function createColumnDistribution(column: string): ColumnDistribution {
|
|
821
|
+
return {
|
|
822
|
+
column,
|
|
823
|
+
counts: new Map(),
|
|
824
|
+
present: 0,
|
|
825
|
+
numeric: 0,
|
|
826
|
+
min: Number.POSITIVE_INFINITY,
|
|
827
|
+
max: Number.NEGATIVE_INFINITY,
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Formats one accumulated column distribution into a single line:
|
|
832
|
+
// "(all empty)", a numeric range, or the top values with their counts.
|
|
833
|
+
function formatColumnDistribution(stats: ColumnDistribution): string {
|
|
834
|
+
const { column, counts, present, numeric, min, max } = stats;
|
|
835
|
+
if (present === 0) {
|
|
836
|
+
return `- ${column}: (all empty)`;
|
|
837
|
+
}
|
|
838
|
+
if (
|
|
839
|
+
numeric === present &&
|
|
840
|
+
counts.size > present * MIN_NUMERIC_DISTINCT_RATIO
|
|
841
|
+
) {
|
|
842
|
+
return `- ${column}: numeric, range ${min}–${max} (${present} values)`;
|
|
843
|
+
}
|
|
844
|
+
const top = Array.from(counts.entries())
|
|
845
|
+
.sort((a, b) => b[1] - a[1])
|
|
846
|
+
.slice(0, MAX_AGGREGATE_VALUES_PER_COLUMN)
|
|
847
|
+
.map(([value, count]) => `${value} (${count})`)
|
|
848
|
+
.join(', ');
|
|
849
|
+
const more =
|
|
850
|
+
counts.size > MAX_AGGREGATE_VALUES_PER_COLUMN
|
|
851
|
+
? `, … ${counts.size} distinct total`
|
|
852
|
+
: '';
|
|
853
|
+
return `- ${column}: ${top}${more}`;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
export function buildDataAggregatesText(
|
|
857
|
+
columns: string[],
|
|
858
|
+
rows: TDSRowDataType[],
|
|
859
|
+
): string {
|
|
860
|
+
if (rows.length === 0) {
|
|
861
|
+
return 'Total rows: 0';
|
|
862
|
+
}
|
|
863
|
+
const distributions = columns
|
|
864
|
+
.slice(0, MAX_AGGREGATE_COLUMNS)
|
|
865
|
+
.map(createColumnDistribution);
|
|
866
|
+
for (const row of rows) {
|
|
867
|
+
for (const stats of distributions) {
|
|
868
|
+
const raw = row[stats.column];
|
|
869
|
+
if (raw === null || raw === undefined || raw === '') {
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
stats.present += 1;
|
|
873
|
+
const asString = String(raw);
|
|
874
|
+
stats.counts.set(asString, (stats.counts.get(asString) ?? 0) + 1);
|
|
875
|
+
const asNumber = typeof raw === 'number' ? raw : Number(asString);
|
|
876
|
+
if (asString.trim() !== '' && !Number.isNaN(asNumber)) {
|
|
877
|
+
stats.numeric += 1;
|
|
878
|
+
stats.min = Math.min(stats.min, asNumber);
|
|
879
|
+
stats.max = Math.max(stats.max, asNumber);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
return [
|
|
884
|
+
`Total rows: ${rows.length}`,
|
|
885
|
+
'Column value distributions over ALL rows (value: count):',
|
|
886
|
+
...distributions.map(formatColumnDistribution),
|
|
887
|
+
].join('\n');
|
|
888
|
+
}
|
|
889
|
+
|
|
722
890
|
function buildDeterministicResultSummary(
|
|
723
891
|
question: string,
|
|
724
892
|
query: string,
|
|
725
893
|
columns: string[],
|
|
726
|
-
rows:
|
|
894
|
+
rows: TDSRowDataType[],
|
|
727
895
|
): string {
|
|
728
896
|
const selectedColumns = columns.length === 0 ? 'none' : columns.join(', ');
|
|
729
897
|
const rowCount = rows.length;
|
|
730
898
|
const previewRows = rows
|
|
731
899
|
.slice(0, ANALYSIS_PREVIEW_ROW_LIMIT)
|
|
732
900
|
.map((row, index) => {
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
return `${index + 1}. ${formattedEntries.join(', ')}`;
|
|
739
|
-
}
|
|
740
|
-
return `${index + 1}. ${formatPreviewValue(row)}`;
|
|
901
|
+
const entries = Object.entries(row).slice(0, 4);
|
|
902
|
+
const formattedEntries = entries.map(
|
|
903
|
+
([key, value]) => `${key}: ${formatPreviewValue(value)}`,
|
|
904
|
+
);
|
|
905
|
+
return `${index + 1}. ${formattedEntries.join(', ')}`;
|
|
741
906
|
})
|
|
742
907
|
.join('\n');
|
|
743
908
|
|
|
@@ -867,7 +1032,11 @@ function finalizeJudgeAttempt(
|
|
|
867
1032
|
maxAttempts: number,
|
|
868
1033
|
setMessages: MessageSetter,
|
|
869
1034
|
): string | null | undefined {
|
|
870
|
-
if (
|
|
1035
|
+
if (
|
|
1036
|
+
normalizeSqlForComparison(currentSql) !==
|
|
1037
|
+
normalizeSqlForComparison(previousSql) &&
|
|
1038
|
+
attempt < maxAttempts
|
|
1039
|
+
) {
|
|
871
1040
|
return undefined;
|
|
872
1041
|
}
|
|
873
1042
|
addThinkingStep(
|
|
@@ -1232,9 +1401,11 @@ export async function analyzeOrchestratorResults(
|
|
|
1232
1401
|
question,
|
|
1233
1402
|
query,
|
|
1234
1403
|
execResult.columns,
|
|
1235
|
-
execResult.rows,
|
|
1404
|
+
execResult.rows.slice(0, MAX_ANALYSIS_ROWS),
|
|
1236
1405
|
metadata,
|
|
1237
1406
|
config,
|
|
1407
|
+
execResult.rows.length,
|
|
1408
|
+
buildDataAggregatesText(execResult.columns, execResult.rows),
|
|
1238
1409
|
),
|
|
1239
1410
|
ANALYSIS_TIMEOUT_MS,
|
|
1240
1411
|
);
|
|
@@ -1360,6 +1531,207 @@ async function retryWithAlternateRoot(
|
|
|
1360
1531
|
}
|
|
1361
1532
|
}
|
|
1362
1533
|
|
|
1534
|
+
async function attemptProbedRegeneration(
|
|
1535
|
+
question: string,
|
|
1536
|
+
legendQuery: string,
|
|
1537
|
+
resolvedEntities: LegendAIResolvedEntities,
|
|
1538
|
+
orchestratorOptions: Required<LegendAIOrchestratorOptionsParam>,
|
|
1539
|
+
metadata: LegendAIProductMetadata,
|
|
1540
|
+
context: LegendAIOperationContext,
|
|
1541
|
+
options: { startTime: number; modelContext?: LegendAIModelContext },
|
|
1542
|
+
): Promise<boolean> {
|
|
1543
|
+
const { startTime, modelContext } = options;
|
|
1544
|
+
if (!modelContext) {
|
|
1545
|
+
return false;
|
|
1546
|
+
}
|
|
1547
|
+
const { dataProductCoordinates, pureExecutionContext } = orchestratorOptions;
|
|
1548
|
+
const { config, plugin, setMessages } = context;
|
|
1549
|
+
|
|
1550
|
+
const rootEntity = modelContext.entities.find(
|
|
1551
|
+
(entity) => entity.path === resolvedEntities.rootEntity,
|
|
1552
|
+
);
|
|
1553
|
+
if (!rootEntity) {
|
|
1554
|
+
return false;
|
|
1555
|
+
}
|
|
1556
|
+
const columns = extractFilteredColumns(legendQuery).filter((column) =>
|
|
1557
|
+
rootEntity.properties.some(
|
|
1558
|
+
(prop) => prop.name === column && !prop.type.includes('::'),
|
|
1559
|
+
),
|
|
1560
|
+
);
|
|
1561
|
+
if (columns.length === 0) {
|
|
1562
|
+
return false;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
addThinkingStep(setMessages, 'Re-grounding with actual column values...');
|
|
1566
|
+
const probeResults = await Promise.all(
|
|
1567
|
+
columns.slice(0, MAX_RECOVERED_FILTER_COLUMNS).map(async (column) => {
|
|
1568
|
+
try {
|
|
1569
|
+
const probe = await withTimeout(
|
|
1570
|
+
plugin.executePureQuery(
|
|
1571
|
+
`${resolvedEntities.rootEntity}.all()->project([col(x|$x.${column}, '${column}')])->distinct()->take(${DISTINCT_PROBE_ROW_LIMIT})`,
|
|
1572
|
+
pureExecutionContext,
|
|
1573
|
+
dataProductCoordinates,
|
|
1574
|
+
config,
|
|
1575
|
+
),
|
|
1576
|
+
ORCHESTRATOR_GENERATION_TIMEOUT_MS,
|
|
1577
|
+
);
|
|
1578
|
+
const values = (probe?.rows ?? [])
|
|
1579
|
+
.map((row) => row[column])
|
|
1580
|
+
.filter(isNonNullable)
|
|
1581
|
+
.map(String);
|
|
1582
|
+
return values.length > 0
|
|
1583
|
+
? { column, values: Array.from(new Set(values)) }
|
|
1584
|
+
: undefined;
|
|
1585
|
+
} catch (probeError) {
|
|
1586
|
+
assertErrorThrown(probeError);
|
|
1587
|
+
addThinkingStep(setMessages, `Could not probe values for "${column}"`);
|
|
1588
|
+
return undefined;
|
|
1589
|
+
}
|
|
1590
|
+
}),
|
|
1591
|
+
);
|
|
1592
|
+
const probed = new Map<string, string[]>();
|
|
1593
|
+
for (const entry of probeResults) {
|
|
1594
|
+
if (entry !== undefined) {
|
|
1595
|
+
probed.set(entry.column, entry.values);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
if (probed.size === 0) {
|
|
1599
|
+
return false;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
const base = buildEnrichedBusinessContext(
|
|
1603
|
+
question,
|
|
1604
|
+
resolvedEntities.rootEntity,
|
|
1605
|
+
resolvedEntities.relatedEntities,
|
|
1606
|
+
modelContext,
|
|
1607
|
+
);
|
|
1608
|
+
const augmentedContext = {
|
|
1609
|
+
...base,
|
|
1610
|
+
businessContextMatch: {
|
|
1611
|
+
...base.businessContextMatch,
|
|
1612
|
+
additionalNlModelContext: [
|
|
1613
|
+
...(base.businessContextMatch?.additionalNlModelContext ?? []),
|
|
1614
|
+
...buildProbedValueHints(probed),
|
|
1615
|
+
],
|
|
1616
|
+
},
|
|
1617
|
+
};
|
|
1618
|
+
try {
|
|
1619
|
+
const response = await withTimeout(
|
|
1620
|
+
plugin.generateQueryViaOrchestrator(
|
|
1621
|
+
{
|
|
1622
|
+
user_question: question,
|
|
1623
|
+
semantic_search_resolution_details: {
|
|
1624
|
+
data_product_coordinates: dataProductCoordinates,
|
|
1625
|
+
root_entity: resolvedEntities.rootEntity,
|
|
1626
|
+
related_entities: resolvedEntities.relatedEntities,
|
|
1627
|
+
enriched_business_context: augmentedContext,
|
|
1628
|
+
},
|
|
1629
|
+
},
|
|
1630
|
+
config,
|
|
1631
|
+
),
|
|
1632
|
+
ORCHESTRATOR_GENERATION_TIMEOUT_MS,
|
|
1633
|
+
);
|
|
1634
|
+
if (!response) {
|
|
1635
|
+
return false;
|
|
1636
|
+
}
|
|
1637
|
+
const safeQuery = ensurePureSafetyLimit(response.legend_query);
|
|
1638
|
+
updateLastAssistant(setMessages, () => ({
|
|
1639
|
+
sql: safeQuery,
|
|
1640
|
+
isExecuting: true,
|
|
1641
|
+
}));
|
|
1642
|
+
const result = await executePureQueryAndReport(
|
|
1643
|
+
safeQuery,
|
|
1644
|
+
pureExecutionContext,
|
|
1645
|
+
dataProductCoordinates,
|
|
1646
|
+
config,
|
|
1647
|
+
plugin,
|
|
1648
|
+
setMessages,
|
|
1649
|
+
startTime,
|
|
1650
|
+
);
|
|
1651
|
+
if (result && result.rows.length > 0) {
|
|
1652
|
+
await analyzeOrchestratorResults(
|
|
1653
|
+
question,
|
|
1654
|
+
safeQuery,
|
|
1655
|
+
result,
|
|
1656
|
+
metadata,
|
|
1657
|
+
context,
|
|
1658
|
+
startTime,
|
|
1659
|
+
);
|
|
1660
|
+
return true;
|
|
1661
|
+
}
|
|
1662
|
+
return false;
|
|
1663
|
+
} catch (regenerateError) {
|
|
1664
|
+
assertErrorThrown(regenerateError);
|
|
1665
|
+
addThinkingStep(
|
|
1666
|
+
setMessages,
|
|
1667
|
+
`Re-grounded retry failed: ${regenerateError.message.slice(
|
|
1668
|
+
0,
|
|
1669
|
+
MAX_THINKING_ERROR_PREVIEW_LENGTH,
|
|
1670
|
+
)}`,
|
|
1671
|
+
);
|
|
1672
|
+
return false;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
// Retries the query with case-insensitive (relaxed) string filters. Returns
|
|
1677
|
+
// true when the relaxed query returned rows and its results were reported.
|
|
1678
|
+
async function attemptRelaxedFilterRetry(
|
|
1679
|
+
question: string,
|
|
1680
|
+
legendQuery: string,
|
|
1681
|
+
orchestratorOptions: Required<LegendAIOrchestratorOptionsParam>,
|
|
1682
|
+
metadata: LegendAIProductMetadata,
|
|
1683
|
+
context: LegendAIOperationContext,
|
|
1684
|
+
startTime: number,
|
|
1685
|
+
): Promise<boolean> {
|
|
1686
|
+
const { config, plugin, setMessages } = context;
|
|
1687
|
+
const { dataProductCoordinates, pureExecutionContext } = orchestratorOptions;
|
|
1688
|
+
const relaxedQuery = relaxExactStringFilters(legendQuery);
|
|
1689
|
+
if (relaxedQuery === legendQuery) {
|
|
1690
|
+
return false;
|
|
1691
|
+
}
|
|
1692
|
+
addThinkingStep(
|
|
1693
|
+
setMessages,
|
|
1694
|
+
'Retrying with relaxed (case-insensitive) filters...',
|
|
1695
|
+
);
|
|
1696
|
+
try {
|
|
1697
|
+
const safeRelaxed = ensurePureSafetyLimit(relaxedQuery);
|
|
1698
|
+
updateLastAssistant(setMessages, () => ({
|
|
1699
|
+
sql: safeRelaxed,
|
|
1700
|
+
isExecuting: true,
|
|
1701
|
+
}));
|
|
1702
|
+
const relaxedResult = await executePureQueryAndReport(
|
|
1703
|
+
safeRelaxed,
|
|
1704
|
+
pureExecutionContext,
|
|
1705
|
+
dataProductCoordinates,
|
|
1706
|
+
config,
|
|
1707
|
+
plugin,
|
|
1708
|
+
setMessages,
|
|
1709
|
+
startTime,
|
|
1710
|
+
);
|
|
1711
|
+
if (relaxedResult && relaxedResult.rows.length > 0) {
|
|
1712
|
+
await analyzeOrchestratorResults(
|
|
1713
|
+
question,
|
|
1714
|
+
relaxedQuery,
|
|
1715
|
+
relaxedResult,
|
|
1716
|
+
metadata,
|
|
1717
|
+
context,
|
|
1718
|
+
startTime,
|
|
1719
|
+
);
|
|
1720
|
+
return true;
|
|
1721
|
+
}
|
|
1722
|
+
} catch (relaxError) {
|
|
1723
|
+
assertErrorThrown(relaxError);
|
|
1724
|
+
addThinkingStep(
|
|
1725
|
+
setMessages,
|
|
1726
|
+
`Relaxed-filter retry failed: ${relaxError.message.slice(
|
|
1727
|
+
0,
|
|
1728
|
+
MAX_THINKING_ERROR_PREVIEW_LENGTH,
|
|
1729
|
+
)}`,
|
|
1730
|
+
);
|
|
1731
|
+
}
|
|
1732
|
+
return false;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1363
1735
|
async function handleEmptyOrchestratorResults(
|
|
1364
1736
|
question: string,
|
|
1365
1737
|
legendQuery: string,
|
|
@@ -1373,7 +1745,35 @@ async function handleEmptyOrchestratorResults(
|
|
|
1373
1745
|
},
|
|
1374
1746
|
): Promise<void> {
|
|
1375
1747
|
const { startTime, modelContext } = options;
|
|
1376
|
-
const {
|
|
1748
|
+
const { setMessages } = context;
|
|
1749
|
+
|
|
1750
|
+
if (
|
|
1751
|
+
await attemptRelaxedFilterRetry(
|
|
1752
|
+
question,
|
|
1753
|
+
legendQuery,
|
|
1754
|
+
orchestratorOptions,
|
|
1755
|
+
metadata,
|
|
1756
|
+
context,
|
|
1757
|
+
startTime,
|
|
1758
|
+
)
|
|
1759
|
+
) {
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
const regenerated = await attemptProbedRegeneration(
|
|
1764
|
+
question,
|
|
1765
|
+
legendQuery,
|
|
1766
|
+
resolvedEntities,
|
|
1767
|
+
orchestratorOptions,
|
|
1768
|
+
metadata,
|
|
1769
|
+
context,
|
|
1770
|
+
{ startTime, ...(modelContext === undefined ? {} : { modelContext }) },
|
|
1771
|
+
);
|
|
1772
|
+
if (regenerated) {
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
const triedRoots = new Set<string>([resolvedEntities.rootEntity]);
|
|
1377
1777
|
|
|
1378
1778
|
if (resolvedEntities.relatedEntities.length > 0) {
|
|
1379
1779
|
const alternateRoot = modelContext
|
|
@@ -1384,6 +1784,7 @@ async function handleEmptyOrchestratorResults(
|
|
|
1384
1784
|
)
|
|
1385
1785
|
: resolvedEntities.relatedEntities[0];
|
|
1386
1786
|
if (alternateRoot) {
|
|
1787
|
+
triedRoots.add(alternateRoot);
|
|
1387
1788
|
const succeeded = await retryWithAlternateRoot(
|
|
1388
1789
|
question,
|
|
1389
1790
|
alternateRoot,
|
|
@@ -1399,24 +1800,56 @@ async function handleEmptyOrchestratorResults(
|
|
|
1399
1800
|
}
|
|
1400
1801
|
}
|
|
1401
1802
|
|
|
1402
|
-
addThinkingStep(
|
|
1803
|
+
addThinkingStep(setMessages, 'No results found for this question.');
|
|
1804
|
+
completeThinkingSteps(setMessages);
|
|
1805
|
+
updateLastAssistant(setMessages, () => ({
|
|
1806
|
+
textAnswer: buildUnanswerableMessage(),
|
|
1807
|
+
suggestedQueries: [],
|
|
1808
|
+
isProcessing: false,
|
|
1809
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
1810
|
+
}));
|
|
1811
|
+
|
|
1812
|
+
attachAlternateRootFallback(
|
|
1813
|
+
question,
|
|
1814
|
+
resolvedEntities,
|
|
1815
|
+
triedRoots,
|
|
1816
|
+
modelContext,
|
|
1403
1817
|
setMessages,
|
|
1404
|
-
'No results returned \u2014 building contextual guidance...',
|
|
1405
1818
|
);
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
// Offers a "try a different root entity" fallback action when a not-yet-tried,
|
|
1822
|
+
// positively-ranked root exists for the question.
|
|
1823
|
+
function attachAlternateRootFallback(
|
|
1824
|
+
question: string,
|
|
1825
|
+
resolvedEntities: LegendAIResolvedEntities,
|
|
1826
|
+
triedRoots: Set<string>,
|
|
1827
|
+
modelContext: LegendAIModelContext | undefined,
|
|
1828
|
+
setMessages: MessageSetter,
|
|
1829
|
+
): void {
|
|
1830
|
+
const nextRoot = modelContext
|
|
1831
|
+
? rankEntities(question, modelContext).find(
|
|
1832
|
+
(ranked) => !triedRoots.has(ranked.entity.path) && ranked.score > 0,
|
|
1833
|
+
)?.entity
|
|
1834
|
+
: undefined;
|
|
1835
|
+
if (!nextRoot) {
|
|
1836
|
+
return;
|
|
1419
1837
|
}
|
|
1838
|
+
const nextRelated = resolvedEntities.relatedEntities.filter(
|
|
1839
|
+
(path) => path !== nextRoot.path,
|
|
1840
|
+
);
|
|
1841
|
+
updateLastAssistant(setMessages, () => ({
|
|
1842
|
+
fallbackAction: {
|
|
1843
|
+
label: `Tried ${extractElementNameFromPath(
|
|
1844
|
+
resolvedEntities.rootEntity,
|
|
1845
|
+
)} as the root \u2014 try ${nextRoot.name} instead`,
|
|
1846
|
+
actionId: LEGEND_AI_ALTERNATE_ROOT_ACTION_ID,
|
|
1847
|
+
resolvedEntities: {
|
|
1848
|
+
rootEntity: nextRoot.path,
|
|
1849
|
+
relatedEntities: nextRelated,
|
|
1850
|
+
},
|
|
1851
|
+
},
|
|
1852
|
+
}));
|
|
1420
1853
|
}
|
|
1421
1854
|
|
|
1422
1855
|
async function resolveOrchestrationEntities(
|
|
@@ -1454,16 +1887,12 @@ function extractUserErrorFromMessage(errorMessage: string): string {
|
|
|
1454
1887
|
const marker = 'with error:';
|
|
1455
1888
|
const idx = errorMessage.toLowerCase().indexOf(marker);
|
|
1456
1889
|
if (idx !== -1) {
|
|
1457
|
-
const
|
|
1458
|
-
const newlineIdx = after.indexOf('\n');
|
|
1459
|
-
const reason = (
|
|
1460
|
-
newlineIdx === -1 ? after : after.slice(0, newlineIdx)
|
|
1461
|
-
).trim();
|
|
1890
|
+
const reason = errorMessage.slice(idx + marker.length).trim();
|
|
1462
1891
|
if (reason.length > 0) {
|
|
1463
|
-
return reason.slice(0,
|
|
1892
|
+
return reason.slice(0, MAX_DISPLAYED_ERROR_LENGTH);
|
|
1464
1893
|
}
|
|
1465
1894
|
}
|
|
1466
|
-
return errorMessage.slice(0,
|
|
1895
|
+
return errorMessage.slice(0, MAX_DISPLAYED_ERROR_LENGTH);
|
|
1467
1896
|
}
|
|
1468
1897
|
|
|
1469
1898
|
async function handleOrchestratorError(
|
|
@@ -1508,27 +1937,104 @@ async function handleOrchestratorError(
|
|
|
1508
1937
|
`Fallback guidance failed: ${fallbackError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
1509
1938
|
);
|
|
1510
1939
|
}
|
|
1511
|
-
finishWithThinkingError(
|
|
1940
|
+
finishWithThinkingError(
|
|
1941
|
+
setMessages,
|
|
1942
|
+
extractUserErrorFromMessage(error.message),
|
|
1943
|
+
startTime,
|
|
1944
|
+
orchErrorType,
|
|
1945
|
+
);
|
|
1512
1946
|
}
|
|
1513
1947
|
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1948
|
+
// Analyzes a non-empty orchestrator result (or runs empty-result recovery),
|
|
1949
|
+
// attaching the model-approach overview when available.
|
|
1950
|
+
async function reportOrchestratorExecutionResult(
|
|
1951
|
+
params: {
|
|
1952
|
+
question: string;
|
|
1953
|
+
safeQuery: string;
|
|
1954
|
+
execResult: LegendAISqlExecutionResultData;
|
|
1955
|
+
orchestratorOptions: Required<LegendAIOrchestratorOptionsParam>;
|
|
1956
|
+
metadata: LegendAIProductMetadata;
|
|
1957
|
+
resolvedEntities: LegendAIResolvedEntities;
|
|
1958
|
+
startTime: number;
|
|
1959
|
+
modelContext?: LegendAIModelContext;
|
|
1960
|
+
},
|
|
1518
1961
|
context: LegendAIOperationContext,
|
|
1519
|
-
pureExecutionContext?: QueryExplicitExecutionContextInfo,
|
|
1520
|
-
preResolvedEntities?: LegendAIResolvedEntities,
|
|
1521
|
-
modelContext?: LegendAIModelContext,
|
|
1522
1962
|
): Promise<void> {
|
|
1523
|
-
const {
|
|
1524
|
-
|
|
1525
|
-
|
|
1963
|
+
const {
|
|
1964
|
+
question,
|
|
1965
|
+
safeQuery,
|
|
1966
|
+
execResult,
|
|
1967
|
+
orchestratorOptions,
|
|
1968
|
+
metadata,
|
|
1969
|
+
resolvedEntities,
|
|
1970
|
+
startTime,
|
|
1971
|
+
modelContext,
|
|
1972
|
+
} = params;
|
|
1973
|
+
const { setMessages } = context;
|
|
1526
1974
|
try {
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1975
|
+
if (execResult.rows.length > 0) {
|
|
1976
|
+
await analyzeOrchestratorResults(
|
|
1977
|
+
question,
|
|
1978
|
+
safeQuery,
|
|
1979
|
+
execResult,
|
|
1980
|
+
metadata,
|
|
1981
|
+
context,
|
|
1982
|
+
startTime,
|
|
1983
|
+
);
|
|
1984
|
+
if (modelContext) {
|
|
1985
|
+
const approachText = buildDataQueryApproachText(
|
|
1986
|
+
question,
|
|
1987
|
+
modelContext,
|
|
1988
|
+
resolvedEntities.rootEntity,
|
|
1989
|
+
);
|
|
1990
|
+
if (approachText) {
|
|
1991
|
+
attachMetadataOverview(setMessages, approachText);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
} else {
|
|
1995
|
+
await handleEmptyOrchestratorResults(
|
|
1996
|
+
question,
|
|
1997
|
+
safeQuery,
|
|
1998
|
+
orchestratorOptions,
|
|
1999
|
+
metadata,
|
|
2000
|
+
resolvedEntities,
|
|
2001
|
+
context,
|
|
2002
|
+
{ startTime, ...(modelContext === undefined ? {} : { modelContext }) },
|
|
2003
|
+
);
|
|
2004
|
+
}
|
|
2005
|
+
} catch (analysisError) {
|
|
2006
|
+
assertErrorThrown(analysisError);
|
|
2007
|
+
addThinkingStep(
|
|
2008
|
+
setMessages,
|
|
2009
|
+
`Result analysis failed: ${analysisError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
2010
|
+
);
|
|
2011
|
+
} finally {
|
|
2012
|
+
completeThinkingSteps(setMessages);
|
|
2013
|
+
updateLastAssistant(setMessages, () => ({
|
|
2014
|
+
isProcessing: false,
|
|
2015
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
2016
|
+
}));
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
export async function processQuestionViaOrchestrator(
|
|
2021
|
+
question: string,
|
|
2022
|
+
dataProductCoordinates: LegendAIOrchestratorDataProductCoordinates,
|
|
2023
|
+
metadata: LegendAIProductMetadata,
|
|
2024
|
+
context: LegendAIOperationContext,
|
|
2025
|
+
pureExecutionContext?: QueryExplicitExecutionContextInfo,
|
|
2026
|
+
preResolvedEntities?: LegendAIResolvedEntities,
|
|
2027
|
+
modelContext?: LegendAIModelContext,
|
|
2028
|
+
): Promise<void> {
|
|
2029
|
+
const { config, plugin, setMessages } = context;
|
|
2030
|
+
const startTime = Date.now();
|
|
2031
|
+
|
|
2032
|
+
try {
|
|
2033
|
+
const resolvedEntities = await resolveOrchestrationEntities(
|
|
2034
|
+
question,
|
|
2035
|
+
dataProductCoordinates,
|
|
2036
|
+
context,
|
|
2037
|
+
pureExecutionContext,
|
|
1532
2038
|
preResolvedEntities,
|
|
1533
2039
|
modelContext,
|
|
1534
2040
|
);
|
|
@@ -1608,43 +2114,19 @@ export async function processQuestionViaOrchestrator(
|
|
|
1608
2114
|
return;
|
|
1609
2115
|
}
|
|
1610
2116
|
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
)
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
safeQuery,
|
|
1625
|
-
{ dataProductCoordinates, pureExecutionContext },
|
|
1626
|
-
metadata,
|
|
1627
|
-
resolvedEntities,
|
|
1628
|
-
context,
|
|
1629
|
-
{
|
|
1630
|
-
startTime,
|
|
1631
|
-
...(modelContext === undefined ? {} : { modelContext }),
|
|
1632
|
-
},
|
|
1633
|
-
);
|
|
1634
|
-
}
|
|
1635
|
-
} catch (analysisError) {
|
|
1636
|
-
assertErrorThrown(analysisError);
|
|
1637
|
-
addThinkingStep(
|
|
1638
|
-
setMessages,
|
|
1639
|
-
`Result analysis failed: ${analysisError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
1640
|
-
);
|
|
1641
|
-
} finally {
|
|
1642
|
-
completeThinkingSteps(setMessages);
|
|
1643
|
-
updateLastAssistant(setMessages, () => ({
|
|
1644
|
-
isProcessing: false,
|
|
1645
|
-
thinkingDuration: elapsedSeconds(startTime),
|
|
1646
|
-
}));
|
|
1647
|
-
}
|
|
2117
|
+
await reportOrchestratorExecutionResult(
|
|
2118
|
+
{
|
|
2119
|
+
question,
|
|
2120
|
+
safeQuery,
|
|
2121
|
+
execResult,
|
|
2122
|
+
orchestratorOptions: { dataProductCoordinates, pureExecutionContext },
|
|
2123
|
+
metadata,
|
|
2124
|
+
resolvedEntities,
|
|
2125
|
+
startTime,
|
|
2126
|
+
...(modelContext === undefined ? {} : { modelContext }),
|
|
2127
|
+
},
|
|
2128
|
+
context,
|
|
2129
|
+
);
|
|
1648
2130
|
} catch (error) {
|
|
1649
2131
|
assertErrorThrown(error);
|
|
1650
2132
|
await handleOrchestratorError(
|
|
@@ -1666,6 +2148,10 @@ export function cleanLlmSqlResponse(raw: string): string {
|
|
|
1666
2148
|
.trim();
|
|
1667
2149
|
}
|
|
1668
2150
|
|
|
2151
|
+
function normalizeSqlForComparison(sql: string): string {
|
|
2152
|
+
return sql.replaceAll(/\s+/gu, ' ').trim().toLowerCase();
|
|
2153
|
+
}
|
|
2154
|
+
|
|
1669
2155
|
export function isValidSqlCorrection(
|
|
1670
2156
|
trimmed: string,
|
|
1671
2157
|
currentSql: string,
|
|
@@ -1677,139 +2163,6 @@ export function isValidSqlCorrection(
|
|
|
1677
2163
|
);
|
|
1678
2164
|
}
|
|
1679
2165
|
|
|
1680
|
-
export function sanitizeJoinOrderBy(sql: string): string {
|
|
1681
|
-
if (!JOIN_PATTERN.test(sql)) {
|
|
1682
|
-
return sql;
|
|
1683
|
-
}
|
|
1684
|
-
const parts = sql.split(ORDER_BY_SPLIT);
|
|
1685
|
-
if (parts.length < 2) {
|
|
1686
|
-
return sql;
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
|
-
const beforeOrderBy = parts[0] ?? '';
|
|
1690
|
-
const afterOrderBy = parts.slice(1).join('ORDER BY').replace(/^\s+/, '');
|
|
1691
|
-
|
|
1692
|
-
const selectAliases = new Map<string, string>();
|
|
1693
|
-
for (const m of beforeOrderBy.matchAll(SELECT_ALIAS_PATTERN)) {
|
|
1694
|
-
const tableAlias = (m.groups?.tbl ?? '').toLowerCase();
|
|
1695
|
-
const colName = (m.groups?.col ?? '').toLowerCase();
|
|
1696
|
-
const asAlias = m.groups?.qAlias ?? m.groups?.uAlias ?? '';
|
|
1697
|
-
selectAliases.set(`${tableAlias}.${colName}`, asAlias);
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
if (selectAliases.size === 0) {
|
|
1701
|
-
return sql;
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
|
-
// Build a lookup of the original alias.col text → its SELECT alias replacement.
|
|
1705
|
-
// matchAll exposes typed `.groups`, so no cast is needed.
|
|
1706
|
-
const replacements = new Map<string, string>();
|
|
1707
|
-
for (const m of afterOrderBy.matchAll(ALIAS_DOT_COL_PATTERN)) {
|
|
1708
|
-
const tbl = (m.groups?.tbl ?? '').toLowerCase();
|
|
1709
|
-
const col = (m.groups?.col ?? '').toLowerCase();
|
|
1710
|
-
const alias = selectAliases.get(`${tbl}.${col}`);
|
|
1711
|
-
if (alias) {
|
|
1712
|
-
replacements.set(m[0], `"${alias}"`);
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
|
|
1716
|
-
if (replacements.size === 0) {
|
|
1717
|
-
return sql;
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
const rewritten = afterOrderBy.replaceAll(
|
|
1721
|
-
ALIAS_DOT_COL_PATTERN,
|
|
1722
|
-
(match) => replacements.get(match) ?? match,
|
|
1723
|
-
);
|
|
1724
|
-
|
|
1725
|
-
if (rewritten === afterOrderBy) {
|
|
1726
|
-
return sql;
|
|
1727
|
-
}
|
|
1728
|
-
return `${beforeOrderBy}ORDER BY ${rewritten}`;
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
export function sanitizeJoinSameKeyColumns(
|
|
1732
|
-
sql: string,
|
|
1733
|
-
services?: TDSServiceSchema[],
|
|
1734
|
-
): string {
|
|
1735
|
-
if (!JOIN_PATTERN.test(sql)) {
|
|
1736
|
-
return sql;
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
const joinRegex =
|
|
1740
|
-
/\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;
|
|
1741
|
-
|
|
1742
|
-
let result = sql;
|
|
1743
|
-
let match: RegExpExecArray | null;
|
|
1744
|
-
|
|
1745
|
-
while ((match = joinRegex.exec(sql)) !== null) {
|
|
1746
|
-
const pId = match.groups?.pId ?? '';
|
|
1747
|
-
const rightAlias = match.groups?.rAlias ?? '';
|
|
1748
|
-
const onClause = (match.groups?.onClause ?? '').slice(0, 500);
|
|
1749
|
-
|
|
1750
|
-
const sameKeyMatch =
|
|
1751
|
-
/(?<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(
|
|
1752
|
-
onClause,
|
|
1753
|
-
);
|
|
1754
|
-
if (!sameKeyMatch) {
|
|
1755
|
-
continue;
|
|
1756
|
-
}
|
|
1757
|
-
|
|
1758
|
-
const leftCol = sameKeyMatch.groups?.lCol ?? '';
|
|
1759
|
-
const rightCol = sameKeyMatch.groups?.rCol ?? '';
|
|
1760
|
-
const matchedRightAlias = sameKeyMatch.groups?.rAl ?? '';
|
|
1761
|
-
|
|
1762
|
-
if (
|
|
1763
|
-
leftCol.toLowerCase() !== rightCol.toLowerCase() ||
|
|
1764
|
-
matchedRightAlias.toLowerCase() !== rightAlias.toLowerCase()
|
|
1765
|
-
) {
|
|
1766
|
-
continue;
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
const renamedKey = `${rightAlias}_${rightCol}`;
|
|
1770
|
-
const columnList = buildSubqueryColumnList(
|
|
1771
|
-
pId,
|
|
1772
|
-
rightCol,
|
|
1773
|
-
renamedKey,
|
|
1774
|
-
services,
|
|
1775
|
-
);
|
|
1776
|
-
const originalFragment = `p('${pId}') AS ${rightAlias}`;
|
|
1777
|
-
const subqueryFragment = `(SELECT ${columnList} FROM p('${pId}')) AS ${rightAlias}`;
|
|
1778
|
-
|
|
1779
|
-
const oldOnRef = `${rightAlias}."${rightCol}"`;
|
|
1780
|
-
const newOnRef = `${rightAlias}."${renamedKey}"`;
|
|
1781
|
-
|
|
1782
|
-
result = result.replace(originalFragment, subqueryFragment);
|
|
1783
|
-
result = result.replaceAll(oldOnRef, newOnRef);
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
return result;
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
|
-
function buildSubqueryColumnList(
|
|
1790
|
-
pId: string,
|
|
1791
|
-
originalKey: string,
|
|
1792
|
-
renamedKey: string,
|
|
1793
|
-
services?: TDSServiceSchema[],
|
|
1794
|
-
): string {
|
|
1795
|
-
const svc = services?.find(
|
|
1796
|
-
(s) =>
|
|
1797
|
-
s.dataProductPath &&
|
|
1798
|
-
s.pattern &&
|
|
1799
|
-
pId === `${s.dataProductPath}.${s.pattern.replace(/^\//, '')}`,
|
|
1800
|
-
);
|
|
1801
|
-
if (!svc || svc.columns.length === 0) {
|
|
1802
|
-
return `"${originalKey}" AS "${renamedKey}", *`;
|
|
1803
|
-
}
|
|
1804
|
-
return svc.columns
|
|
1805
|
-
.map((c) =>
|
|
1806
|
-
c.name.toLowerCase() === originalKey.toLowerCase()
|
|
1807
|
-
? `"${c.name}" AS "${renamedKey}"`
|
|
1808
|
-
: `"${c.name}"`,
|
|
1809
|
-
)
|
|
1810
|
-
.join(', ');
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
2166
|
export function sanitizeLiteralColumns(sql: string): string {
|
|
1814
2167
|
if (!UNION_ALL_PATTERN.test(sql)) {
|
|
1815
2168
|
return sql;
|
|
@@ -2111,14 +2464,15 @@ export function ensureSafeLimit(
|
|
|
2111
2464
|
}
|
|
2112
2465
|
|
|
2113
2466
|
function prepareSafeSql(sql: string, services: TDSServiceSchema[]): string {
|
|
2114
|
-
const
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
sanitizeJoinSameKeyColumns(sanitizeJoinOrderBy(sql), services),
|
|
2118
|
-
),
|
|
2467
|
+
const joined = sanitizeLiteralColumns(
|
|
2468
|
+
sanitizeJoinSameKeyColumns(
|
|
2469
|
+
sanitizeJoinOrderBy(sanitizeJoinDuplicateColumns(sql, services)),
|
|
2119
2470
|
services,
|
|
2120
2471
|
),
|
|
2121
2472
|
);
|
|
2473
|
+
const wrapped = wrapBareJoinAccessPoints(joined, services);
|
|
2474
|
+
const bounded = boundCrossAccessPointJoinDrivingSide(wrapped);
|
|
2475
|
+
const safeSql = ensureSafeLimit(ensureDateParameters(bounded, services));
|
|
2122
2476
|
const unsupported = detectUnsupportedEnginePattern(safeSql);
|
|
2123
2477
|
if (unsupported) {
|
|
2124
2478
|
throw new LegendAIUnsupportedEngineShapeError(unsupported.hint);
|
|
@@ -2147,7 +2501,14 @@ async function executeSqlForServices(
|
|
|
2147
2501
|
(s) => s.sourceType === TDSServiceSourceType.ACCESS_POINT,
|
|
2148
2502
|
);
|
|
2149
2503
|
if (isAccessPoint && dataProductCoordinates) {
|
|
2150
|
-
|
|
2504
|
+
const result = await withTimeout(
|
|
2505
|
+
plugin.executeLakehouseSql(safeSql, dataProductCoordinates, config),
|
|
2506
|
+
AP_SQL_EXECUTION_TIMEOUT_MS,
|
|
2507
|
+
);
|
|
2508
|
+
if (result === undefined) {
|
|
2509
|
+
throw new LegendAIExecutionTimeoutError(AP_SQL_EXECUTION_TIMEOUT_MESSAGE);
|
|
2510
|
+
}
|
|
2511
|
+
return result;
|
|
2151
2512
|
}
|
|
2152
2513
|
return plugin.executeSql(safeSql, config);
|
|
2153
2514
|
}
|
|
@@ -2183,6 +2544,9 @@ async function executeSqlWithRetries(
|
|
|
2183
2544
|
return { sql: currentSql, result };
|
|
2184
2545
|
} catch (executeError) {
|
|
2185
2546
|
assertErrorThrown(executeError);
|
|
2547
|
+
if (executeError instanceof LegendAIExecutionTimeoutError) {
|
|
2548
|
+
return { sql: currentSql, error: executeError.message };
|
|
2549
|
+
}
|
|
2186
2550
|
if (attempt >= DEFAULT_MAX_EXECUTION_RETRIES) {
|
|
2187
2551
|
return { sql: currentSql, error: executeError.message };
|
|
2188
2552
|
}
|
|
@@ -2353,7 +2717,11 @@ function buildZeroRowMessage(services: TDSServiceSchema[]): string {
|
|
|
2353
2717
|
return `The SQL query executed successfully but returned **0 rows**. Note: parameter detection for ${names} was incomplete — this service may require additional parameters not shown in the schema. Try specifying filter values (dates, IDs, etc.) directly in your question.`;
|
|
2354
2718
|
}
|
|
2355
2719
|
|
|
2356
|
-
return
|
|
2720
|
+
return buildUnanswerableMessage();
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
function buildUnanswerableMessage(): string {
|
|
2724
|
+
return `Sorry — I couldn't find an answer to this one. This assistant is still learning this data product, so it may not handle every question yet. ${LEGEND_AI_FEEDBACK_PROMPT} Use the 👍 / 👎 below — your feedback helps us improve. You can also try rephrasing with a specific column or value.`;
|
|
2357
2725
|
}
|
|
2358
2726
|
|
|
2359
2727
|
function handleSqlGenerationFailure(
|
|
@@ -2393,6 +2761,7 @@ interface QueryResultReport {
|
|
|
2393
2761
|
sqlResult: LegendAISqlExecutionResultData;
|
|
2394
2762
|
question: string;
|
|
2395
2763
|
services: TDSServiceSchema[];
|
|
2764
|
+
allAccessPoints?: TDSServiceSchema[];
|
|
2396
2765
|
}
|
|
2397
2766
|
|
|
2398
2767
|
async function reportQueryResults(
|
|
@@ -2402,8 +2771,8 @@ async function reportQueryResults(
|
|
|
2402
2771
|
startTime: number,
|
|
2403
2772
|
hasOrchestratorFallback: boolean,
|
|
2404
2773
|
): Promise<void> {
|
|
2405
|
-
const { currentSql, sqlResult, question, services } = report;
|
|
2406
|
-
const { setMessages } = context;
|
|
2774
|
+
const { currentSql, sqlResult, question, services, allAccessPoints } = report;
|
|
2775
|
+
const { config, plugin, setMessages } = context;
|
|
2407
2776
|
if (sqlResult.rows.length > 0) {
|
|
2408
2777
|
const columns = deduplicateColumns(sqlResult.columns);
|
|
2409
2778
|
const rows = sqlResult.rows;
|
|
@@ -2452,7 +2821,34 @@ async function reportQueryResults(
|
|
|
2452
2821
|
setMessages,
|
|
2453
2822
|
'Query returned 0 rows after correction attempts.',
|
|
2454
2823
|
);
|
|
2824
|
+
const joinExplanation = buildCrossJoinZeroRowExplanation(
|
|
2825
|
+
currentSql,
|
|
2826
|
+
services,
|
|
2827
|
+
);
|
|
2828
|
+
const joinSuggestions =
|
|
2829
|
+
joinExplanation === undefined
|
|
2830
|
+
? []
|
|
2831
|
+
: buildJoinablePairSuggestions(services, allAccessPoints ?? services);
|
|
2832
|
+
const llmAnalysis = await withTimeout(
|
|
2833
|
+
plugin
|
|
2834
|
+
.buildNoResultsFallback(
|
|
2835
|
+
question,
|
|
2836
|
+
currentSql,
|
|
2837
|
+
metadata,
|
|
2838
|
+
config,
|
|
2839
|
+
joinExplanation,
|
|
2840
|
+
)
|
|
2841
|
+
.catch((error: unknown) => {
|
|
2842
|
+
assertErrorThrown(error);
|
|
2843
|
+
return undefined;
|
|
2844
|
+
}),
|
|
2845
|
+
ANALYSIS_TIMEOUT_MS,
|
|
2846
|
+
);
|
|
2455
2847
|
completeThinkingSteps(setMessages);
|
|
2848
|
+
const suggestions =
|
|
2849
|
+
joinSuggestions.length > 0
|
|
2850
|
+
? joinSuggestions
|
|
2851
|
+
: (llmAnalysis?.suggestedQueries ?? []);
|
|
2456
2852
|
const fallback = hasOrchestratorFallback
|
|
2457
2853
|
? {
|
|
2458
2854
|
fallbackAction: {
|
|
@@ -2462,7 +2858,11 @@ async function reportQueryResults(
|
|
|
2462
2858
|
}
|
|
2463
2859
|
: {};
|
|
2464
2860
|
updateLastAssistant(setMessages, () => ({
|
|
2465
|
-
textAnswer:
|
|
2861
|
+
textAnswer:
|
|
2862
|
+
llmAnalysis?.summary ??
|
|
2863
|
+
joinExplanation ??
|
|
2864
|
+
buildZeroRowMessage(services),
|
|
2865
|
+
...(suggestions.length > 0 ? { suggestedQueries: suggestions } : {}),
|
|
2466
2866
|
...fallback,
|
|
2467
2867
|
isProcessing: false,
|
|
2468
2868
|
isExecuting: false,
|
|
@@ -2471,11 +2871,13 @@ async function reportQueryResults(
|
|
|
2471
2871
|
}
|
|
2472
2872
|
}
|
|
2473
2873
|
|
|
2874
|
+
const DEFAULT_MAX_SUPPLEMENTED_SERVICES = 4;
|
|
2875
|
+
|
|
2474
2876
|
export function supplementMissingCoverage(
|
|
2475
2877
|
question: string,
|
|
2476
2878
|
selected: TDSServiceSchema[],
|
|
2477
2879
|
allServices: TDSServiceSchema[],
|
|
2478
|
-
maxTotal: number =
|
|
2880
|
+
maxTotal: number = DEFAULT_MAX_SUPPLEMENTED_SERVICES,
|
|
2479
2881
|
): TDSServiceSchema[] {
|
|
2480
2882
|
if (selected.length >= maxTotal || allServices.length <= selected.length) {
|
|
2481
2883
|
return selected;
|
|
@@ -2692,6 +3094,403 @@ async function selectBestServices(
|
|
|
2692
3094
|
}
|
|
2693
3095
|
}
|
|
2694
3096
|
|
|
3097
|
+
const EQUALITY_PREDICATE_PATTERN =
|
|
3098
|
+
/"(?<col>[^"]+)"\s*=\s*'(?<val>[^']*(?:''[^']*)*)'/gu;
|
|
3099
|
+
export const IN_PREDICATE_PATTERN =
|
|
3100
|
+
/"(?<col>[^"]+)"\s+in\s*\((?<list>[^)]+)\)/giu;
|
|
3101
|
+
export const IN_LIST_LITERAL_PATTERN = /'(?<val>[^']*(?:''[^']*)*)'/gu;
|
|
3102
|
+
const MIN_FUZZY_LITERAL_LENGTH = 3;
|
|
3103
|
+
|
|
3104
|
+
function normalizeFilterLiteral(value: string): string {
|
|
3105
|
+
return value.replaceAll(/[^a-z0-9]/giu, '').toUpperCase();
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
function escapeSqlStringLiteral(value: string): string {
|
|
3109
|
+
return value.replaceAll("'", "''");
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
export function extractFromClause(sql: string): string | undefined {
|
|
3113
|
+
const fromMatch = /\bfrom\b/iu.exec(sql);
|
|
3114
|
+
if (!fromMatch) {
|
|
3115
|
+
return undefined;
|
|
3116
|
+
}
|
|
3117
|
+
const rest = sql.slice(fromMatch.index);
|
|
3118
|
+
const afterFrom = rest.slice('from'.length);
|
|
3119
|
+
const boundary = /\b(?:where|group\s+by|order\s+by|having|limit)\b/iu.exec(
|
|
3120
|
+
afterFrom,
|
|
3121
|
+
);
|
|
3122
|
+
return (
|
|
3123
|
+
boundary ? rest.slice(0, 'from'.length + boundary.index) : rest
|
|
3124
|
+
).trim();
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
// Extracts the WHERE clause segment (from the first `WHERE` up to the next
|
|
3128
|
+
// top-level clause boundary), so filter self-healing only rewrites WHERE
|
|
3129
|
+
// predicates — never JOIN ON keys or HAVING literals.
|
|
3130
|
+
export function extractWhereClause(sql: string): string | undefined {
|
|
3131
|
+
const whereMatch = /\bwhere\b/iu.exec(sql);
|
|
3132
|
+
if (!whereMatch) {
|
|
3133
|
+
return undefined;
|
|
3134
|
+
}
|
|
3135
|
+
const rest = sql.slice(whereMatch.index);
|
|
3136
|
+
const afterWhere = rest.slice('where'.length);
|
|
3137
|
+
const boundary =
|
|
3138
|
+
/\b(?:group\s+by|order\s+by|having|limit|window|qualify)\b/iu.exec(
|
|
3139
|
+
afterWhere,
|
|
3140
|
+
);
|
|
3141
|
+
return (
|
|
3142
|
+
boundary ? rest.slice(0, 'where'.length + boundary.index) : rest
|
|
3143
|
+
).trim();
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
export function resolveFilterLiteral(
|
|
3147
|
+
literal: string,
|
|
3148
|
+
values: string[],
|
|
3149
|
+
): string | undefined {
|
|
3150
|
+
if (values.includes(literal)) {
|
|
3151
|
+
return undefined;
|
|
3152
|
+
}
|
|
3153
|
+
const caseInsensitive = Array.from(
|
|
3154
|
+
new Set(values.filter((v) => v.toLowerCase() === literal.toLowerCase())),
|
|
3155
|
+
);
|
|
3156
|
+
if (caseInsensitive.length === 1) {
|
|
3157
|
+
return caseInsensitive[0];
|
|
3158
|
+
}
|
|
3159
|
+
const normalizedLiteral = normalizeFilterLiteral(literal);
|
|
3160
|
+
if (normalizedLiteral.length < MIN_FUZZY_LITERAL_LENGTH) {
|
|
3161
|
+
return undefined;
|
|
3162
|
+
}
|
|
3163
|
+
const normalizedExact = Array.from(
|
|
3164
|
+
new Set(
|
|
3165
|
+
values.filter((v) => normalizeFilterLiteral(v) === normalizedLiteral),
|
|
3166
|
+
),
|
|
3167
|
+
);
|
|
3168
|
+
if (normalizedExact.length === 1) {
|
|
3169
|
+
return normalizedExact[0];
|
|
3170
|
+
}
|
|
3171
|
+
const prefixMatches = Array.from(
|
|
3172
|
+
new Set(
|
|
3173
|
+
values.filter((v) => {
|
|
3174
|
+
const normalizedValue = normalizeFilterLiteral(v);
|
|
3175
|
+
return (
|
|
3176
|
+
normalizedValue.length >= MIN_FUZZY_LITERAL_LENGTH &&
|
|
3177
|
+
(normalizedValue.startsWith(normalizedLiteral) ||
|
|
3178
|
+
normalizedLiteral.startsWith(normalizedValue))
|
|
3179
|
+
);
|
|
3180
|
+
}),
|
|
3181
|
+
),
|
|
3182
|
+
);
|
|
3183
|
+
return prefixMatches.length === 1 ? prefixMatches[0] : undefined;
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
type FilterValueProbe = (column: string) => Promise<string[] | undefined>;
|
|
3187
|
+
|
|
3188
|
+
// Rewrites `"col" = 'literal'` predicates in the WHERE clause whose literal
|
|
3189
|
+
// fuzzy-matches a real probed column value, returning the updated SQL and
|
|
3190
|
+
// whether anything changed. Every distinct predicate is corrected (a column can
|
|
3191
|
+
// appear with several literals across `OR` branches), and only the WHERE clause
|
|
3192
|
+
// is scanned so JOIN ON / HAVING literals are left untouched.
|
|
3193
|
+
async function correctEqualityFilters(
|
|
3194
|
+
sql: string,
|
|
3195
|
+
probeColumnValues: FilterValueProbe,
|
|
3196
|
+
setMessages: MessageSetter,
|
|
3197
|
+
): Promise<{ sql: string; corrected: boolean }> {
|
|
3198
|
+
const whereClause = extractWhereClause(sql);
|
|
3199
|
+
if (whereClause === undefined) {
|
|
3200
|
+
return { sql, corrected: false };
|
|
3201
|
+
}
|
|
3202
|
+
const literalsByColumn = new Map<string, Set<string>>();
|
|
3203
|
+
for (const match of whereClause.matchAll(EQUALITY_PREDICATE_PATTERN)) {
|
|
3204
|
+
const column = match.groups?.col;
|
|
3205
|
+
const literal = match.groups?.val;
|
|
3206
|
+
if (column === undefined || literal === undefined) {
|
|
3207
|
+
continue;
|
|
3208
|
+
}
|
|
3209
|
+
const literals = literalsByColumn.get(column) ?? new Set<string>();
|
|
3210
|
+
literals.add(literal);
|
|
3211
|
+
literalsByColumn.set(column, literals);
|
|
3212
|
+
}
|
|
3213
|
+
const resolutions = new Map<string, Map<string, string>>();
|
|
3214
|
+
for (const [column, literals] of literalsByColumn) {
|
|
3215
|
+
const values = await probeColumnValues(column);
|
|
3216
|
+
if (values === undefined) {
|
|
3217
|
+
continue;
|
|
3218
|
+
}
|
|
3219
|
+
for (const literal of literals) {
|
|
3220
|
+
const resolved = resolveFilterLiteral(literal, values);
|
|
3221
|
+
if (resolved !== undefined && resolved !== literal) {
|
|
3222
|
+
const columnResolutions = resolutions.get(column) ?? new Map();
|
|
3223
|
+
columnResolutions.set(literal, resolved);
|
|
3224
|
+
resolutions.set(column, columnResolutions);
|
|
3225
|
+
addThinkingStep(
|
|
3226
|
+
setMessages,
|
|
3227
|
+
`Matched "${column}" = '${literal}' to '${resolved}' from the column's values...`,
|
|
3228
|
+
);
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
if (resolutions.size === 0) {
|
|
3233
|
+
return { sql, corrected: false };
|
|
3234
|
+
}
|
|
3235
|
+
const correctedWhere = whereClause.replace(
|
|
3236
|
+
EQUALITY_PREDICATE_PATTERN,
|
|
3237
|
+
(matched: string, column: string, literal: string) => {
|
|
3238
|
+
const resolved = resolutions.get(column)?.get(literal);
|
|
3239
|
+
return resolved === undefined
|
|
3240
|
+
? matched
|
|
3241
|
+
: matched.replace(
|
|
3242
|
+
`'${literal}'`,
|
|
3243
|
+
() => `'${escapeSqlStringLiteral(resolved)}'`,
|
|
3244
|
+
);
|
|
3245
|
+
},
|
|
3246
|
+
);
|
|
3247
|
+
return {
|
|
3248
|
+
sql: sql.replace(whereClause, () => correctedWhere),
|
|
3249
|
+
corrected: true,
|
|
3250
|
+
};
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
// Rewrites the literals of a single `IN (...)` list to their fuzzy-matched
|
|
3254
|
+
// column values; returns the joined list text and whether anything changed.
|
|
3255
|
+
function rewriteInListLiterals(
|
|
3256
|
+
list: string,
|
|
3257
|
+
column: string,
|
|
3258
|
+
values: string[],
|
|
3259
|
+
setMessages: MessageSetter,
|
|
3260
|
+
): { rewritten: string; changed: boolean } {
|
|
3261
|
+
let changed = false;
|
|
3262
|
+
const rewrittenList: string[] = [];
|
|
3263
|
+
for (const literalMatch of list.matchAll(IN_LIST_LITERAL_PATTERN)) {
|
|
3264
|
+
const literal = literalMatch.groups?.val;
|
|
3265
|
+
if (literal === undefined) {
|
|
3266
|
+
continue;
|
|
3267
|
+
}
|
|
3268
|
+
const resolved = resolveFilterLiteral(literal, values);
|
|
3269
|
+
if (resolved !== undefined && resolved !== literal) {
|
|
3270
|
+
rewrittenList.push(`'${escapeSqlStringLiteral(resolved)}'`);
|
|
3271
|
+
changed = true;
|
|
3272
|
+
addThinkingStep(
|
|
3273
|
+
setMessages,
|
|
3274
|
+
`Matched "${column}" IN '${literal}' to '${resolved}' from the column's values...`,
|
|
3275
|
+
);
|
|
3276
|
+
} else {
|
|
3277
|
+
rewrittenList.push(`'${literal}'`);
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
return { rewritten: rewrittenList.join(', '), changed };
|
|
3281
|
+
}
|
|
3282
|
+
|
|
3283
|
+
async function correctInPredicateFilters(
|
|
3284
|
+
sql: string,
|
|
3285
|
+
probeColumnValues: FilterValueProbe,
|
|
3286
|
+
setMessages: MessageSetter,
|
|
3287
|
+
): Promise<{ sql: string; corrected: boolean }> {
|
|
3288
|
+
const whereClause = extractWhereClause(sql);
|
|
3289
|
+
if (whereClause === undefined) {
|
|
3290
|
+
return { sql, corrected: false };
|
|
3291
|
+
}
|
|
3292
|
+
const columns = new Set<string>();
|
|
3293
|
+
for (const match of whereClause.matchAll(IN_PREDICATE_PATTERN)) {
|
|
3294
|
+
const column = match.groups?.col;
|
|
3295
|
+
if (column !== undefined) {
|
|
3296
|
+
columns.add(column);
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
const valuesByColumn = new Map<string, string[]>();
|
|
3300
|
+
for (const column of columns) {
|
|
3301
|
+
const values = await probeColumnValues(column);
|
|
3302
|
+
if (values !== undefined) {
|
|
3303
|
+
valuesByColumn.set(column, values);
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
if (valuesByColumn.size === 0) {
|
|
3307
|
+
return { sql, corrected: false };
|
|
3308
|
+
}
|
|
3309
|
+
const correctedWhere = whereClause.replace(
|
|
3310
|
+
IN_PREDICATE_PATTERN,
|
|
3311
|
+
(matched: string, column: string, list: string) => {
|
|
3312
|
+
const values = valuesByColumn.get(column);
|
|
3313
|
+
if (values === undefined) {
|
|
3314
|
+
return matched;
|
|
3315
|
+
}
|
|
3316
|
+
const { rewritten, changed } = rewriteInListLiterals(
|
|
3317
|
+
list,
|
|
3318
|
+
column,
|
|
3319
|
+
values,
|
|
3320
|
+
setMessages,
|
|
3321
|
+
);
|
|
3322
|
+
return changed && rewritten.length > 0
|
|
3323
|
+
? matched.replace(list, () => rewritten)
|
|
3324
|
+
: matched;
|
|
3325
|
+
},
|
|
3326
|
+
);
|
|
3327
|
+
return correctedWhere === whereClause
|
|
3328
|
+
? { sql, corrected: false }
|
|
3329
|
+
: { sql: sql.replace(whereClause, () => correctedWhere), corrected: true };
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
// Distinct columns filtered by equality or IN predicates in the WHERE clause,
|
|
3333
|
+
// in encounter order — the set the zero-row recovery probes for real values.
|
|
3334
|
+
function collectWhereFilterColumns(sql: string): string[] {
|
|
3335
|
+
const whereClause = extractWhereClause(sql);
|
|
3336
|
+
if (whereClause === undefined) {
|
|
3337
|
+
return [];
|
|
3338
|
+
}
|
|
3339
|
+
const columns: string[] = [];
|
|
3340
|
+
const seen = new Set<string>();
|
|
3341
|
+
const addColumn = (column: string | undefined): void => {
|
|
3342
|
+
if (column !== undefined && !seen.has(column)) {
|
|
3343
|
+
seen.add(column);
|
|
3344
|
+
columns.push(column);
|
|
3345
|
+
}
|
|
3346
|
+
};
|
|
3347
|
+
for (const match of whereClause.matchAll(EQUALITY_PREDICATE_PATTERN)) {
|
|
3348
|
+
addColumn(match.groups?.col);
|
|
3349
|
+
}
|
|
3350
|
+
for (const match of whereClause.matchAll(IN_PREDICATE_PATTERN)) {
|
|
3351
|
+
addColumn(match.groups?.col);
|
|
3352
|
+
}
|
|
3353
|
+
return columns;
|
|
3354
|
+
}
|
|
3355
|
+
|
|
3356
|
+
// Indexes each access point by its lowercased column names (first writer wins),
|
|
3357
|
+
// so a filter column resolves to the access point that declares it.
|
|
3358
|
+
function buildAccessPointColumnIndex(
|
|
3359
|
+
services: TDSServiceSchema[],
|
|
3360
|
+
): Map<string, TDSServiceSchema> {
|
|
3361
|
+
const index = new Map<string, TDSServiceSchema>();
|
|
3362
|
+
for (const accessPoint of services) {
|
|
3363
|
+
if (accessPoint.sourceType !== TDSServiceSourceType.ACCESS_POINT) {
|
|
3364
|
+
continue;
|
|
3365
|
+
}
|
|
3366
|
+
for (const column of accessPoint.columns) {
|
|
3367
|
+
const key = column.name.toLowerCase();
|
|
3368
|
+
if (!index.has(key)) {
|
|
3369
|
+
index.set(key, accessPoint);
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
return index;
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
// Recovers a zero-row query by re-probing each WHERE-filter column's real distinct
|
|
3377
|
+
// values and rewriting equality/IN literals that don't match (casing/spelling).
|
|
3378
|
+
async function attemptDistinctValueRecovery(
|
|
3379
|
+
currentSql: string,
|
|
3380
|
+
selectedServices: TDSServiceSchema[],
|
|
3381
|
+
dataProductCoordinates:
|
|
3382
|
+
| LegendAIOrchestratorDataProductCoordinates
|
|
3383
|
+
| undefined,
|
|
3384
|
+
context: LegendAIOperationContext,
|
|
3385
|
+
): Promise<
|
|
3386
|
+
{ sql: string; result: LegendAISqlExecutionResultData } | undefined
|
|
3387
|
+
> {
|
|
3388
|
+
const { plugin, config, setMessages } = context;
|
|
3389
|
+
const isAccessPointProbe =
|
|
3390
|
+
dataProductCoordinates !== undefined &&
|
|
3391
|
+
selectedServices.some(
|
|
3392
|
+
(s) => s.sourceType === TDSServiceSourceType.ACCESS_POINT,
|
|
3393
|
+
);
|
|
3394
|
+
const accessPointByColumn = isAccessPointProbe
|
|
3395
|
+
? buildAccessPointColumnIndex(selectedServices)
|
|
3396
|
+
: new Map<string, TDSServiceSchema>();
|
|
3397
|
+
const fromClause = isAccessPointProbe
|
|
3398
|
+
? undefined
|
|
3399
|
+
: extractFromClause(currentSql);
|
|
3400
|
+
if (!isAccessPointProbe && fromClause === undefined) {
|
|
3401
|
+
return undefined;
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
const probeAccessPointColumn = async (
|
|
3405
|
+
column: string,
|
|
3406
|
+
): Promise<LegendAISqlExecutionResultData> => {
|
|
3407
|
+
const accessPoint = accessPointByColumn.get(column.toLowerCase());
|
|
3408
|
+
const pId = accessPoint ? servicePId(accessPoint) : undefined;
|
|
3409
|
+
if (pId === undefined) {
|
|
3410
|
+
return { columns: [], rows: [] };
|
|
3411
|
+
}
|
|
3412
|
+
const relationQuery = `#P{${pId}}#->select(~[${pureRelationColumnRef(column)}])->distinct()->take(${DISTINCT_PROBE_ROW_LIMIT})`;
|
|
3413
|
+
return plugin.executeLakehouseRelationQuery(
|
|
3414
|
+
relationQuery,
|
|
3415
|
+
guaranteeNonNullable(dataProductCoordinates),
|
|
3416
|
+
config,
|
|
3417
|
+
);
|
|
3418
|
+
};
|
|
3419
|
+
|
|
3420
|
+
const probedValues = new Map<string, string[] | undefined>();
|
|
3421
|
+
const probeColumnValues = async (
|
|
3422
|
+
column: string,
|
|
3423
|
+
): Promise<string[] | undefined> => {
|
|
3424
|
+
if (probedValues.has(column)) {
|
|
3425
|
+
return probedValues.get(column);
|
|
3426
|
+
}
|
|
3427
|
+
if (probedValues.size >= MAX_RECOVERED_FILTER_COLUMNS) {
|
|
3428
|
+
return undefined;
|
|
3429
|
+
}
|
|
3430
|
+
let values: string[] | undefined;
|
|
3431
|
+
try {
|
|
3432
|
+
const probe = isAccessPointProbe
|
|
3433
|
+
? await probeAccessPointColumn(column)
|
|
3434
|
+
: await executeSqlForServices(
|
|
3435
|
+
`SELECT DISTINCT "${column}" ${fromClause} LIMIT ${DISTINCT_PROBE_ROW_LIMIT}`,
|
|
3436
|
+
selectedServices,
|
|
3437
|
+
dataProductCoordinates,
|
|
3438
|
+
plugin,
|
|
3439
|
+
config,
|
|
3440
|
+
);
|
|
3441
|
+
values = probe.rows
|
|
3442
|
+
.map((row) => row[column])
|
|
3443
|
+
.filter(isNonNullable)
|
|
3444
|
+
.map(String);
|
|
3445
|
+
} catch (probeError) {
|
|
3446
|
+
assertErrorThrown(probeError);
|
|
3447
|
+
addThinkingStep(setMessages, `Could not probe values for "${column}"`);
|
|
3448
|
+
values = undefined;
|
|
3449
|
+
}
|
|
3450
|
+
probedValues.set(column, values);
|
|
3451
|
+
return values;
|
|
3452
|
+
};
|
|
3453
|
+
|
|
3454
|
+
await Promise.all(
|
|
3455
|
+
collectWhereFilterColumns(currentSql)
|
|
3456
|
+
.slice(0, MAX_RECOVERED_FILTER_COLUMNS)
|
|
3457
|
+
.map((column) => probeColumnValues(column)),
|
|
3458
|
+
);
|
|
3459
|
+
|
|
3460
|
+
const equalityPass = await correctEqualityFilters(
|
|
3461
|
+
currentSql,
|
|
3462
|
+
probeColumnValues,
|
|
3463
|
+
setMessages,
|
|
3464
|
+
);
|
|
3465
|
+
const inPass = await correctInPredicateFilters(
|
|
3466
|
+
equalityPass.sql,
|
|
3467
|
+
probeColumnValues,
|
|
3468
|
+
setMessages,
|
|
3469
|
+
);
|
|
3470
|
+
const correctedSql = inPass.sql;
|
|
3471
|
+
const corrected = equalityPass.corrected || inPass.corrected;
|
|
3472
|
+
|
|
3473
|
+
if (!corrected) {
|
|
3474
|
+
return undefined;
|
|
3475
|
+
}
|
|
3476
|
+
try {
|
|
3477
|
+
const retryResult = await executeSqlForServices(
|
|
3478
|
+
correctedSql,
|
|
3479
|
+
selectedServices,
|
|
3480
|
+
dataProductCoordinates,
|
|
3481
|
+
plugin,
|
|
3482
|
+
config,
|
|
3483
|
+
);
|
|
3484
|
+
if (retryResult.rows.length > 0) {
|
|
3485
|
+
return { sql: correctedSql, result: retryResult };
|
|
3486
|
+
}
|
|
3487
|
+
} catch (retryError) {
|
|
3488
|
+
assertErrorThrown(retryError);
|
|
3489
|
+
addThinkingStep(setMessages, 'Filter-recovery retry failed');
|
|
3490
|
+
}
|
|
3491
|
+
return undefined;
|
|
3492
|
+
}
|
|
3493
|
+
|
|
2695
3494
|
async function tryRecoverZeroRows(
|
|
2696
3495
|
currentSql: string,
|
|
2697
3496
|
sqlResult: LegendAISqlExecutionResultData,
|
|
@@ -2702,10 +3501,15 @@ async function tryRecoverZeroRows(
|
|
|
2702
3501
|
| LegendAIOrchestratorDataProductCoordinates
|
|
2703
3502
|
| undefined,
|
|
2704
3503
|
context: LegendAIOperationContext,
|
|
2705
|
-
): Promise<{
|
|
3504
|
+
): Promise<{
|
|
3505
|
+
sql: string;
|
|
3506
|
+
result: LegendAISqlExecutionResultData;
|
|
3507
|
+
selfHealed: LegendAISelfHealKind;
|
|
3508
|
+
}> {
|
|
2706
3509
|
const { plugin, config, setMessages } = context;
|
|
2707
3510
|
let recoveredSql = currentSql;
|
|
2708
3511
|
let recoveredResult = sqlResult;
|
|
3512
|
+
let selfHealed = LegendAISelfHealKind.NONE;
|
|
2709
3513
|
|
|
2710
3514
|
const strippedSql = stripGuessedNonDateServiceParams(recoveredSql, question);
|
|
2711
3515
|
if (strippedSql !== recoveredSql) {
|
|
@@ -2735,6 +3539,25 @@ async function tryRecoverZeroRows(
|
|
|
2735
3539
|
}
|
|
2736
3540
|
}
|
|
2737
3541
|
|
|
3542
|
+
if (recoveredResult.rows.length === 0) {
|
|
3543
|
+
addThinkingStep(
|
|
3544
|
+
setMessages,
|
|
3545
|
+
"Checking filter values against the column's actual values...",
|
|
3546
|
+
);
|
|
3547
|
+
const resolved = await attemptDistinctValueRecovery(
|
|
3548
|
+
recoveredSql,
|
|
3549
|
+
selectedServices,
|
|
3550
|
+
dataProductCoordinates,
|
|
3551
|
+
context,
|
|
3552
|
+
);
|
|
3553
|
+
if (resolved) {
|
|
3554
|
+
recoveredSql = resolved.sql;
|
|
3555
|
+
recoveredResult = resolved.result;
|
|
3556
|
+
selfHealed = LegendAISelfHealKind.DISTINCT_VALUE;
|
|
3557
|
+
updateLastAssistant(setMessages, () => ({ sql: resolved.sql }));
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
|
|
2738
3561
|
if (recoveredResult.rows.length === 0) {
|
|
2739
3562
|
const correction = await attemptZeroRowCorrection(
|
|
2740
3563
|
recoveredSql,
|
|
@@ -2747,10 +3570,11 @@ async function tryRecoverZeroRows(
|
|
|
2747
3570
|
if (correction) {
|
|
2748
3571
|
recoveredSql = correction.sql;
|
|
2749
3572
|
recoveredResult = correction.result;
|
|
3573
|
+
selfHealed = LegendAISelfHealKind.LLM_ZERO_ROW;
|
|
2750
3574
|
}
|
|
2751
3575
|
}
|
|
2752
3576
|
|
|
2753
|
-
return { sql: recoveredSql, result: recoveredResult };
|
|
3577
|
+
return { sql: recoveredSql, result: recoveredResult, selfHealed };
|
|
2754
3578
|
}
|
|
2755
3579
|
|
|
2756
3580
|
async function resolveNestedPCalls(
|
|
@@ -2874,6 +3698,86 @@ async function retryAccessPointSqlFix(
|
|
|
2874
3698
|
* - Uses AP-specific generator/judge prompts (p() syntax, no coordinates/params)
|
|
2875
3699
|
* - Skips parameter detection (APs have no parameters)
|
|
2876
3700
|
*/
|
|
3701
|
+
// Handles a failed access-point SQL execution: attempts an SQL-fix retry for
|
|
3702
|
+
// fixable errors, otherwise reports the execution error with fallback suggestions.
|
|
3703
|
+
async function handleAccessPointExecutionError(
|
|
3704
|
+
executeError: Error,
|
|
3705
|
+
params: {
|
|
3706
|
+
finalSql: string;
|
|
3707
|
+
question: string;
|
|
3708
|
+
selectedAPs: TDSServiceSchema[];
|
|
3709
|
+
dataProductCoordinates: LegendAIOrchestratorDataProductCoordinates;
|
|
3710
|
+
modelContextEnrichment?: string;
|
|
3711
|
+
startTime: number;
|
|
3712
|
+
execStartTime: number;
|
|
3713
|
+
metadata: LegendAIProductMetadata;
|
|
3714
|
+
},
|
|
3715
|
+
context: LegendAIOperationContext,
|
|
3716
|
+
): Promise<void> {
|
|
3717
|
+
const { setMessages } = context;
|
|
3718
|
+
const {
|
|
3719
|
+
finalSql,
|
|
3720
|
+
question,
|
|
3721
|
+
selectedAPs,
|
|
3722
|
+
dataProductCoordinates,
|
|
3723
|
+
modelContextEnrichment,
|
|
3724
|
+
startTime,
|
|
3725
|
+
execStartTime,
|
|
3726
|
+
metadata,
|
|
3727
|
+
} = params;
|
|
3728
|
+
const execErrorType = classifyError(executeError);
|
|
3729
|
+
const errMsg = executeError.message;
|
|
3730
|
+
const errorCategory = categorizeExecutionError(errMsg, executeError);
|
|
3731
|
+
|
|
3732
|
+
addThinkingStep(
|
|
3733
|
+
setMessages,
|
|
3734
|
+
`Execution failed: ${errMsg.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`,
|
|
3735
|
+
);
|
|
3736
|
+
|
|
3737
|
+
if (errorCategory === ExecutionErrorCategory.SQL_FIXABLE) {
|
|
3738
|
+
const retried = await retryAccessPointSqlFix(
|
|
3739
|
+
{
|
|
3740
|
+
failedSql: finalSql,
|
|
3741
|
+
question,
|
|
3742
|
+
errMsg,
|
|
3743
|
+
selectedAPs,
|
|
3744
|
+
dataProductCoordinates,
|
|
3745
|
+
...(modelContextEnrichment === undefined
|
|
3746
|
+
? {}
|
|
3747
|
+
: { modelContextEnrichment }),
|
|
3748
|
+
startTime,
|
|
3749
|
+
},
|
|
3750
|
+
context,
|
|
3751
|
+
metadata,
|
|
3752
|
+
);
|
|
3753
|
+
if (retried) {
|
|
3754
|
+
return;
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3758
|
+
finishWithThinkingError(
|
|
3759
|
+
setMessages,
|
|
3760
|
+
buildExecutionErrorMessage(errMsg, selectedAPs),
|
|
3761
|
+
startTime,
|
|
3762
|
+
execErrorType === LegendAIErrorType.GENERAL
|
|
3763
|
+
? LegendAIErrorType.EXECUTION
|
|
3764
|
+
: execErrorType,
|
|
3765
|
+
);
|
|
3766
|
+
const queriedGroups = [
|
|
3767
|
+
...new Set(
|
|
3768
|
+
selectedAPs
|
|
3769
|
+
.map((ap) => ap.accessPointGroupTitle)
|
|
3770
|
+
.filter((t): t is string => t !== undefined),
|
|
3771
|
+
),
|
|
3772
|
+
];
|
|
3773
|
+
updateLastAssistant(setMessages, () => ({
|
|
3774
|
+
execTime: elapsedSeconds(execStartTime, 2),
|
|
3775
|
+
isExecuting: false,
|
|
3776
|
+
suggestedQueries: buildFallbackSuggestions(selectedAPs),
|
|
3777
|
+
queriedAccessPointGroups: queriedGroups,
|
|
3778
|
+
}));
|
|
3779
|
+
}
|
|
3780
|
+
|
|
2877
3781
|
async function processAccessPointQuery(
|
|
2878
3782
|
question: string,
|
|
2879
3783
|
accessPoints: TDSServiceSchema[],
|
|
@@ -2889,10 +3793,33 @@ async function processAccessPointQuery(
|
|
|
2889
3793
|
|
|
2890
3794
|
const selectedAPs = await selectBestServices(question, accessPoints, context);
|
|
2891
3795
|
|
|
3796
|
+
const currentAccessPoints = new Set(
|
|
3797
|
+
selectedAPs.map((ap) => ap.pattern.replace(/^\//, '')),
|
|
3798
|
+
);
|
|
3799
|
+
const scopedContext: LegendAIOperationContext = {
|
|
3800
|
+
...context,
|
|
3801
|
+
history: filterHistoryForAccessPoints(context.history, currentAccessPoints),
|
|
3802
|
+
};
|
|
3803
|
+
|
|
3804
|
+
try {
|
|
3805
|
+
addThinkingStep(setMessages, 'Grounding filter values from real data...');
|
|
3806
|
+
await withTimeout(
|
|
3807
|
+
plugin.enrichAccessPointSampleValues(
|
|
3808
|
+
selectedAPs,
|
|
3809
|
+
dataProductCoordinates,
|
|
3810
|
+
config,
|
|
3811
|
+
),
|
|
3812
|
+
VALUE_GROUNDING_TIME_BUDGET_MS,
|
|
3813
|
+
);
|
|
3814
|
+
} catch (error) {
|
|
3815
|
+
assertErrorThrown(error);
|
|
3816
|
+
addThinkingStep(setMessages, 'Value grounding skipped');
|
|
3817
|
+
}
|
|
3818
|
+
|
|
2892
3819
|
const judgedSql = await generateAndJudgeAccessPointSql(
|
|
2893
3820
|
question,
|
|
2894
3821
|
selectedAPs,
|
|
2895
|
-
|
|
3822
|
+
scopedContext,
|
|
2896
3823
|
startTime,
|
|
2897
3824
|
modelContextEnrichment,
|
|
2898
3825
|
);
|
|
@@ -2921,7 +3848,7 @@ async function processAccessPointQuery(
|
|
|
2921
3848
|
judgedSql,
|
|
2922
3849
|
question,
|
|
2923
3850
|
selectedAPs,
|
|
2924
|
-
|
|
3851
|
+
scopedContext,
|
|
2925
3852
|
modelContextEnrichment,
|
|
2926
3853
|
);
|
|
2927
3854
|
|
|
@@ -2929,83 +3856,69 @@ async function processAccessPointQuery(
|
|
|
2929
3856
|
sql: finalSql,
|
|
2930
3857
|
sqlGenTime: sqlGenTimeValue,
|
|
2931
3858
|
isExecuting: true,
|
|
3859
|
+
queriedAccessPoints: orderedAccessPointsFromSql(finalSql),
|
|
2932
3860
|
}));
|
|
2933
3861
|
|
|
2934
3862
|
const execStartTime = Date.now();
|
|
2935
3863
|
try {
|
|
2936
|
-
|
|
2937
|
-
|
|
3864
|
+
let execSql = finalSql;
|
|
3865
|
+
let rawResult = await executeSqlForServices(
|
|
3866
|
+
execSql,
|
|
2938
3867
|
selectedAPs,
|
|
2939
3868
|
dataProductCoordinates,
|
|
2940
3869
|
plugin,
|
|
2941
3870
|
config,
|
|
2942
3871
|
);
|
|
2943
3872
|
|
|
3873
|
+
if (rawResult.rows.length === 0) {
|
|
3874
|
+
addThinkingStep(
|
|
3875
|
+
setMessages,
|
|
3876
|
+
"Checking filter values against the column's actual values...",
|
|
3877
|
+
);
|
|
3878
|
+
const resolved = await attemptDistinctValueRecovery(
|
|
3879
|
+
execSql,
|
|
3880
|
+
selectedAPs,
|
|
3881
|
+
dataProductCoordinates,
|
|
3882
|
+
scopedContext,
|
|
3883
|
+
);
|
|
3884
|
+
if (resolved) {
|
|
3885
|
+
execSql = resolved.sql;
|
|
3886
|
+
rawResult = resolved.result;
|
|
3887
|
+
updateLastAssistant(setMessages, () => ({ sql: execSql }));
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
3890
|
+
|
|
2944
3891
|
await reportQueryResults(
|
|
2945
3892
|
{
|
|
2946
|
-
currentSql:
|
|
3893
|
+
currentSql: execSql,
|
|
2947
3894
|
sqlResult: rawResult,
|
|
2948
3895
|
question,
|
|
2949
3896
|
services: selectedAPs,
|
|
3897
|
+
allAccessPoints: accessPoints,
|
|
2950
3898
|
},
|
|
2951
3899
|
metadata,
|
|
2952
|
-
|
|
3900
|
+
scopedContext,
|
|
2953
3901
|
startTime,
|
|
2954
3902
|
false,
|
|
2955
3903
|
);
|
|
2956
3904
|
} catch (executeError) {
|
|
2957
3905
|
assertErrorThrown(executeError);
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
failedSql: finalSql,
|
|
2971
|
-
question,
|
|
2972
|
-
errMsg,
|
|
2973
|
-
selectedAPs,
|
|
2974
|
-
dataProductCoordinates,
|
|
2975
|
-
...(modelContextEnrichment === undefined
|
|
2976
|
-
? {}
|
|
2977
|
-
: { modelContextEnrichment }),
|
|
2978
|
-
startTime,
|
|
2979
|
-
},
|
|
2980
|
-
context,
|
|
3906
|
+
await handleAccessPointExecutionError(
|
|
3907
|
+
executeError,
|
|
3908
|
+
{
|
|
3909
|
+
finalSql,
|
|
3910
|
+
question,
|
|
3911
|
+
selectedAPs,
|
|
3912
|
+
dataProductCoordinates,
|
|
3913
|
+
...(modelContextEnrichment === undefined
|
|
3914
|
+
? {}
|
|
3915
|
+
: { modelContextEnrichment }),
|
|
3916
|
+
startTime,
|
|
3917
|
+
execStartTime,
|
|
2981
3918
|
metadata,
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
return;
|
|
2985
|
-
}
|
|
2986
|
-
}
|
|
2987
|
-
|
|
2988
|
-
finishWithThinkingError(
|
|
2989
|
-
setMessages,
|
|
2990
|
-
buildExecutionErrorMessage(errMsg, selectedAPs),
|
|
2991
|
-
startTime,
|
|
2992
|
-
execErrorType === LegendAIErrorType.GENERAL
|
|
2993
|
-
? LegendAIErrorType.EXECUTION
|
|
2994
|
-
: execErrorType,
|
|
3919
|
+
},
|
|
3920
|
+
scopedContext,
|
|
2995
3921
|
);
|
|
2996
|
-
const queriedGroups = [
|
|
2997
|
-
...new Set(
|
|
2998
|
-
selectedAPs
|
|
2999
|
-
.map((ap) => ap.accessPointGroupTitle)
|
|
3000
|
-
.filter((t): t is string => t !== undefined),
|
|
3001
|
-
),
|
|
3002
|
-
];
|
|
3003
|
-
updateLastAssistant(setMessages, () => ({
|
|
3004
|
-
execTime: elapsedSeconds(execStartTime, 2),
|
|
3005
|
-
isExecuting: false,
|
|
3006
|
-
suggestedQueries: buildFallbackSuggestions(selectedAPs),
|
|
3007
|
-
queriedAccessPointGroups: queriedGroups,
|
|
3008
|
-
}));
|
|
3009
3922
|
}
|
|
3010
3923
|
}
|
|
3011
3924
|
|
|
@@ -3018,6 +3931,7 @@ async function processDataQuery(
|
|
|
3018
3931
|
startTime: number,
|
|
3019
3932
|
orchestratorOptions?: LegendAIOrchestratorOptionsParam,
|
|
3020
3933
|
modelContextEnrichment?: string,
|
|
3934
|
+
approachText?: string,
|
|
3021
3935
|
): Promise<void> {
|
|
3022
3936
|
const { config, setMessages } = context;
|
|
3023
3937
|
const dataProductCoordinates = orchestratorOptions?.dataProductCoordinates;
|
|
@@ -3172,6 +4086,10 @@ async function processDataQuery(
|
|
|
3172
4086
|
startTime,
|
|
3173
4087
|
hasOrchestratorFallback,
|
|
3174
4088
|
);
|
|
4089
|
+
|
|
4090
|
+
if (approachText) {
|
|
4091
|
+
attachMetadataOverview(setMessages, approachText);
|
|
4092
|
+
}
|
|
3175
4093
|
}
|
|
3176
4094
|
|
|
3177
4095
|
function splitServicesByType(services: TDSServiceSchema[]): {
|
|
@@ -3197,6 +4115,7 @@ async function routeToAccessPointOrData(
|
|
|
3197
4115
|
startTime: number,
|
|
3198
4116
|
orchestratorOpts: LegendAIOrchestratorOptionsParam | undefined,
|
|
3199
4117
|
modelContextEnrichment?: string,
|
|
4118
|
+
approachText?: string,
|
|
3200
4119
|
): Promise<void> {
|
|
3201
4120
|
const dataProductCoordinates = orchestratorOpts?.dataProductCoordinates;
|
|
3202
4121
|
const { tdsServices, accessPoints } = splitServicesByType(services);
|
|
@@ -3224,10 +4143,30 @@ async function routeToAccessPointOrData(
|
|
|
3224
4143
|
startTime,
|
|
3225
4144
|
orchestratorOpts,
|
|
3226
4145
|
modelContextEnrichment,
|
|
4146
|
+
approachText,
|
|
3227
4147
|
);
|
|
3228
4148
|
}
|
|
3229
4149
|
}
|
|
3230
4150
|
|
|
4151
|
+
/**
|
|
4152
|
+
* Compute a value on first use and cache it. The data-query and metadata
|
|
4153
|
+
* answer paths each need a different (potentially large) model-context string,
|
|
4154
|
+
* but only one path runs per question. Deferring the build means we never pay
|
|
4155
|
+
* to construct the string the chosen path doesn't consume, and the build
|
|
4156
|
+
* happens inside the caller's try/catch rather than eagerly at entry.
|
|
4157
|
+
*/
|
|
4158
|
+
function lazyMemo<T>(compute: () => T): () => T {
|
|
4159
|
+
let computed = false;
|
|
4160
|
+
let value: T;
|
|
4161
|
+
return (): T => {
|
|
4162
|
+
if (!computed) {
|
|
4163
|
+
value = compute();
|
|
4164
|
+
computed = true;
|
|
4165
|
+
}
|
|
4166
|
+
return value;
|
|
4167
|
+
};
|
|
4168
|
+
}
|
|
4169
|
+
|
|
3231
4170
|
export async function processQuestion(
|
|
3232
4171
|
question: string,
|
|
3233
4172
|
services: TDSServiceSchema[],
|
|
@@ -3240,9 +4179,19 @@ export async function processQuestion(
|
|
|
3240
4179
|
): Promise<void> {
|
|
3241
4180
|
const { config, plugin, setMessages } = context;
|
|
3242
4181
|
const startTime = Date.now();
|
|
3243
|
-
const
|
|
3244
|
-
|
|
3245
|
-
|
|
4182
|
+
const getModelContextEnrichment = lazyMemo(() =>
|
|
4183
|
+
modelContext
|
|
4184
|
+
? buildModelContextEnrichmentText(modelContext, services)
|
|
4185
|
+
: undefined,
|
|
4186
|
+
);
|
|
4187
|
+
const getMetadataEnrichment = lazyMemo(() =>
|
|
4188
|
+
modelContext ? buildModelCatalogText(modelContext) : undefined,
|
|
4189
|
+
);
|
|
4190
|
+
const getApproachText = lazyMemo(() =>
|
|
4191
|
+
modelContext
|
|
4192
|
+
? buildDataQueryApproachText(question, modelContext)
|
|
4193
|
+
: undefined,
|
|
4194
|
+
);
|
|
3246
4195
|
|
|
3247
4196
|
try {
|
|
3248
4197
|
addThinkingStep(setMessages, 'Analyzing your question...');
|
|
@@ -3264,7 +4213,7 @@ export async function processQuestion(
|
|
|
3264
4213
|
startTime,
|
|
3265
4214
|
false,
|
|
3266
4215
|
services,
|
|
3267
|
-
|
|
4216
|
+
getMetadataEnrichment(),
|
|
3268
4217
|
);
|
|
3269
4218
|
if (config.orchestratorUrl && dataProductCoordinates) {
|
|
3270
4219
|
updateLastAssistant(setMessages, () => ({
|
|
@@ -3293,7 +4242,7 @@ export async function processQuestion(
|
|
|
3293
4242
|
startTime,
|
|
3294
4243
|
true,
|
|
3295
4244
|
services,
|
|
3296
|
-
|
|
4245
|
+
getMetadataEnrichment(),
|
|
3297
4246
|
);
|
|
3298
4247
|
return;
|
|
3299
4248
|
}
|
|
@@ -3307,7 +4256,8 @@ export async function processQuestion(
|
|
|
3307
4256
|
context,
|
|
3308
4257
|
startTime,
|
|
3309
4258
|
orchestratorOpts,
|
|
3310
|
-
|
|
4259
|
+
getModelContextEnrichment(),
|
|
4260
|
+
getApproachText(),
|
|
3311
4261
|
);
|
|
3312
4262
|
} catch (sqlError) {
|
|
3313
4263
|
assertErrorThrown(sqlError);
|
|
@@ -3322,7 +4272,7 @@ export async function processQuestion(
|
|
|
3322
4272
|
startTime,
|
|
3323
4273
|
true,
|
|
3324
4274
|
services,
|
|
3325
|
-
|
|
4275
|
+
getMetadataEnrichment(),
|
|
3326
4276
|
);
|
|
3327
4277
|
appendFallbackSuggestions(setMessages, services);
|
|
3328
4278
|
}
|
|
@@ -3350,6 +4300,7 @@ async function routeDataQueryWithErrorHandling(
|
|
|
3350
4300
|
context: LegendAIOperationContext,
|
|
3351
4301
|
orchestratorOptions: LegendAIOrchestratorOptionsParam | undefined,
|
|
3352
4302
|
modelContextEnrichment?: string,
|
|
4303
|
+
approachText?: string,
|
|
3353
4304
|
): Promise<void> {
|
|
3354
4305
|
const { setMessages } = context;
|
|
3355
4306
|
const startTime = Date.now();
|
|
@@ -3364,6 +4315,7 @@ async function routeDataQueryWithErrorHandling(
|
|
|
3364
4315
|
startTime,
|
|
3365
4316
|
orchestratorOptions,
|
|
3366
4317
|
modelContextEnrichment,
|
|
4318
|
+
approachText,
|
|
3367
4319
|
);
|
|
3368
4320
|
} catch (error) {
|
|
3369
4321
|
assertErrorThrown(error);
|
|
@@ -3392,9 +4344,19 @@ export async function processQuestionWithIntent(
|
|
|
3392
4344
|
): Promise<void> {
|
|
3393
4345
|
const { config, setMessages } = context;
|
|
3394
4346
|
const dataProductCoordinates = orchestratorOptions?.dataProductCoordinates;
|
|
3395
|
-
const
|
|
3396
|
-
|
|
3397
|
-
|
|
4347
|
+
const getModelContextEnrichment = lazyMemo(() =>
|
|
4348
|
+
modelContext
|
|
4349
|
+
? buildModelContextEnrichmentText(modelContext, services)
|
|
4350
|
+
: undefined,
|
|
4351
|
+
);
|
|
4352
|
+
const getMetadataEnrichment = lazyMemo(() =>
|
|
4353
|
+
modelContext ? buildModelCatalogText(modelContext) : undefined,
|
|
4354
|
+
);
|
|
4355
|
+
const getApproachText = lazyMemo(() =>
|
|
4356
|
+
modelContext
|
|
4357
|
+
? buildDataQueryApproachText(question, modelContext)
|
|
4358
|
+
: undefined,
|
|
4359
|
+
);
|
|
3398
4360
|
|
|
3399
4361
|
if (intent === LegendAIQuestionIntent.METADATA) {
|
|
3400
4362
|
const startTime = Date.now();
|
|
@@ -3406,7 +4368,7 @@ export async function processQuestionWithIntent(
|
|
|
3406
4368
|
startTime,
|
|
3407
4369
|
services.length > 0,
|
|
3408
4370
|
services,
|
|
3409
|
-
|
|
4371
|
+
getMetadataEnrichment(),
|
|
3410
4372
|
);
|
|
3411
4373
|
} catch (error) {
|
|
3412
4374
|
assertErrorThrown(error);
|
|
@@ -3437,7 +4399,8 @@ export async function processQuestionWithIntent(
|
|
|
3437
4399
|
metadata,
|
|
3438
4400
|
context,
|
|
3439
4401
|
orchestratorOptions,
|
|
3440
|
-
|
|
4402
|
+
getModelContextEnrichment(),
|
|
4403
|
+
getApproachText(),
|
|
3441
4404
|
);
|
|
3442
4405
|
return;
|
|
3443
4406
|
}
|
|
@@ -3449,7 +4412,7 @@ export async function processQuestionWithIntent(
|
|
|
3449
4412
|
startTime,
|
|
3450
4413
|
false,
|
|
3451
4414
|
services,
|
|
3452
|
-
|
|
4415
|
+
getMetadataEnrichment(),
|
|
3453
4416
|
);
|
|
3454
4417
|
updateLastAssistant(setMessages, () => ({
|
|
3455
4418
|
fallbackAction: {
|
|
@@ -3467,6 +4430,7 @@ export async function processQuestionWithIntent(
|
|
|
3467
4430
|
metadata,
|
|
3468
4431
|
context,
|
|
3469
4432
|
orchestratorOptions,
|
|
3470
|
-
|
|
4433
|
+
getModelContextEnrichment(),
|
|
4434
|
+
getApproachText(),
|
|
3471
4435
|
);
|
|
3472
4436
|
}
|