@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
|
@@ -13,34 +13,39 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import { assertErrorThrown,
|
|
17
|
-
import { LegendAIQuestionIntent, LegendAIResponseOutcome, LegendAIThinkingStepStatus, LegendAIMessageRole, LegendAIErrorType, LegendAIServiceError, LegendAIUnsupportedEngineShapeError, TDSServiceSourceType, buildColumnDefsFromNames, LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID, getTodayISO, } from '../LegendAITypes.js';
|
|
16
|
+
import { assertErrorThrown, guaranteeNonNullable, isNonNullable, uuid, } from '@finos/legend-shared';
|
|
17
|
+
import { LegendAISelfHealKind, LegendAIQuestionIntent, LegendAIResponseOutcome, LegendAIThinkingStepStatus, LegendAIMessageRole, LegendAIErrorType, LegendAIExecutionTimeoutError, LegendAIServiceError, LegendAIUnsupportedEngineShapeError, TDSServiceSourceType, buildColumnDefsFromNames, LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID, LEGEND_AI_ALTERNATE_ROOT_ACTION_ID, LEGEND_AI_FEEDBACK_PROMPT, getTodayISO, } from '../LegendAITypes.js';
|
|
18
18
|
import { LegendAIJudgeVerdict, } from '../LegendAI_LegendApplicationPlugin_Extension.js';
|
|
19
19
|
import { extractElementNameFromPath, } from '@finos/legend-graph';
|
|
20
|
-
import { buildEnrichedBusinessContext, buildModelContextEnrichmentText, findBestAlternateRoot, splitIdentifierTokens, tokenizeText, } from '../LegendAIDocEnrichment.js';
|
|
20
|
+
import { buildEnrichedBusinessContext, buildModelContextEnrichmentText, buildModelCatalogText, buildDataQueryApproachText, findBestAlternateRoot, rankEntities, relaxExactStringFilters, extractFilteredColumns, buildProbedValueHints, splitIdentifierTokens, tokenizeText, } from '../LegendAIDocEnrichment.js';
|
|
21
21
|
import { isFuzzyMatch, preFilterServicesByRelevance, } from '../LegendAIServiceRetrieval.js';
|
|
22
22
|
import { isNumericColumn, isStringColumn, } from '../components/LegendAIChatHelpers.js';
|
|
23
|
+
import { HAS_LIMIT_PATTERN, AP_CALL_PATTERN, servicePId, pureRelationColumnRef, } from './LegendAISqlHelpers.js';
|
|
24
|
+
import { buildCrossJoinZeroRowExplanation, buildJoinablePairSuggestions, } from './LegendAIJoinAnalysis.js';
|
|
25
|
+
import { boundCrossAccessPointJoinDrivingSide, sanitizeJoinDuplicateColumns, sanitizeJoinOrderBy, sanitizeJoinSameKeyColumns, wrapBareJoinAccessPoints, } from './LegendAISqlJoinSanitizers.js';
|
|
23
26
|
const MAX_ERROR_MESSAGE_LENGTH = 500;
|
|
27
|
+
const MAX_DISPLAYED_ERROR_LENGTH = 4000;
|
|
24
28
|
const MAX_THINKING_ERROR_PREVIEW_LENGTH = 200;
|
|
25
29
|
const DEFAULT_MAX_JUDGE_ATTEMPTS = 5;
|
|
26
30
|
const DEFAULT_MAX_EXECUTION_RETRIES = 3;
|
|
27
31
|
const ANALYSIS_TIMEOUT_MS = 15_000;
|
|
28
32
|
const ORCHESTRATOR_GENERATION_TIMEOUT_MS = 120_000;
|
|
33
|
+
const DISTINCT_PROBE_ROW_LIMIT = 5000;
|
|
34
|
+
const MAX_RECOVERED_FILTER_COLUMNS = 4;
|
|
29
35
|
const EXECUTION_TIMEOUT_MS = 300_000;
|
|
30
36
|
const ANALYSIS_PREVIEW_ROW_LIMIT = 3;
|
|
31
37
|
const ANALYSIS_PREVIEW_VALUE_LIMIT = 40;
|
|
38
|
+
const MAX_ANALYSIS_ROWS = 100;
|
|
32
39
|
const MAX_NON_SQL_PASS_ATTEMPTS = 2;
|
|
33
|
-
const JOIN_PATTERN = /\bJOIN\b/i;
|
|
34
|
-
const ORDER_BY_SPLIT = /\bORDER\s+BY\b/i;
|
|
35
40
|
const UNION_ALL_PATTERN = /\bUNION\s+ALL\b/i;
|
|
36
41
|
const LITERAL_COL_PATTERN = /,\s*'[^']*'\s+AS\s+(?:"[^"]+"|[a-z]\w*)/gi;
|
|
37
|
-
const SELECT_ALIAS_PATTERN = /\b(?<tbl>[a-z]\w*)\s*\.\s*"(?<col>[^"]+)"\s+AS\s+(?:"(?<qAlias>[^"]+)"|(?<uAlias>\w+))/gi;
|
|
38
|
-
const ALIAS_DOT_COL_PATTERN = /\b(?<tbl>[a-z]\w*)\s*\.\s*"(?<col>[^"]+)"/gi;
|
|
39
42
|
const SERVICE_CALL_PATTERN = /\bservice\s*\([^()]*\)/gi;
|
|
40
43
|
const DEFAULT_SAFETY_LIMIT = 1000;
|
|
41
|
-
const
|
|
42
|
-
const HAS_AGGREGATION_PATTERN = /\bGROUP\s+BY\b|\bCOUNT\s*\(|\bSUM\s*\(|\bAVG\s*\(|\bMIN\s*\(|\bMAX\s*\(/i;
|
|
44
|
+
const HAS_AGGREGATION_PATTERN = /\bGROUP\s+BY\b|\b(?:COUNT|SUM|AVG|MIN|MAX)\s*\(|\bSELECT\s+DISTINCT\b|\bHAVING\b|\bQUALIFY\b/i;
|
|
43
45
|
const MAX_SERVICES_FOR_LLM_SELECTION = 30;
|
|
46
|
+
const VALUE_GROUNDING_TIME_BUDGET_MS = 4000;
|
|
47
|
+
const AP_SQL_EXECUTION_TIMEOUT_MS = 90_000;
|
|
48
|
+
const AP_SQL_EXECUTION_TIMEOUT_MESSAGE = '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.';
|
|
44
49
|
const ORCHESTRATOR_FALLBACK_LABEL = 'Try Legend AI Orchestrator';
|
|
45
50
|
const SQL_GENERATION_FAILURE_WITH_ORCHESTRATOR = 'SQL generation could not handle this query. You can try the Legend AI Orchestrator to generate a Pure query instead.';
|
|
46
51
|
const SQL_GENERATION_FAILURE_NO_ORCHESTRATOR = 'SQL generation could not handle this query. Try rephrasing your question.';
|
|
@@ -50,6 +55,11 @@ const SERVICE_PARAM_DATE_LIKE_PATTERNS = [
|
|
|
50
55
|
/start|end|from|until|begin|report|cutoff|valuation|pricing/i,
|
|
51
56
|
];
|
|
52
57
|
const GENERIC_TABLE_PATTERNS = /combined|consolidated|all|master|summary/i;
|
|
58
|
+
const MIN_ALL_NULL_COLUMNS_FOR_JOIN_HINT = 2;
|
|
59
|
+
const JOIN_HINT_COLUMN_PREVIEW_LIMIT = 3;
|
|
60
|
+
const MAX_AGGREGATE_COLUMNS = 12;
|
|
61
|
+
const MAX_AGGREGATE_VALUES_PER_COLUMN = 8;
|
|
62
|
+
const MIN_NUMERIC_DISTINCT_RATIO = 0.5;
|
|
53
63
|
function isLikelySqlQuery(text) {
|
|
54
64
|
const trimmed = text.trim().toLowerCase();
|
|
55
65
|
return (trimmed.startsWith('select') ||
|
|
@@ -102,6 +112,7 @@ export function createMessagePair(text) {
|
|
|
102
112
|
suggestedQueries: [],
|
|
103
113
|
fallbackAction: null,
|
|
104
114
|
queriedAccessPointGroups: [],
|
|
115
|
+
queriedAccessPoints: [],
|
|
105
116
|
},
|
|
106
117
|
];
|
|
107
118
|
}
|
|
@@ -173,6 +184,9 @@ const EXECUTION_ERROR_RULES = [
|
|
|
173
184
|
},
|
|
174
185
|
];
|
|
175
186
|
export function categorizeExecutionError(errMsg, error) {
|
|
187
|
+
if (error instanceof LegendAIExecutionTimeoutError) {
|
|
188
|
+
return ExecutionErrorCategory.NONE;
|
|
189
|
+
}
|
|
176
190
|
if (error instanceof LegendAIUnsupportedEngineShapeError) {
|
|
177
191
|
return ExecutionErrorCategory.SQL_FIXABLE;
|
|
178
192
|
}
|
|
@@ -187,17 +201,52 @@ export function categorizeExecutionError(errMsg, error) {
|
|
|
187
201
|
}
|
|
188
202
|
return ExecutionErrorCategory.NONE;
|
|
189
203
|
}
|
|
204
|
+
const ACCESS_ERROR_PATTERN = /insufficient privileges|access.*denied|permission.*denied|not authorized|unauthorized|403|entitlement/i;
|
|
205
|
+
// Whether an execution error message indicates an access/permission problem (as
|
|
206
|
+
// opposed to a SQL compilation or schema error), gating the "Request Access" UI.
|
|
207
|
+
export function looksLikeAccessError(errorMsg) {
|
|
208
|
+
return errorMsg !== undefined && ACCESS_ERROR_PATTERN.test(errorMsg);
|
|
209
|
+
}
|
|
190
210
|
export function finishWithThinkingError(setMessages, errorMsg, startTime, errorType) {
|
|
191
211
|
updateLastAssistant(setMessages, (msg) => ({
|
|
192
212
|
thinkingSteps: msg.thinkingSteps.map((s) => s.status === LegendAIThinkingStepStatus.ACTIVE
|
|
193
213
|
? { ...s, status: LegendAIThinkingStepStatus.ERROR }
|
|
194
214
|
: s),
|
|
195
|
-
error: errorMsg.slice(0,
|
|
215
|
+
error: errorMsg.slice(0, MAX_DISPLAYED_ERROR_LENGTH),
|
|
196
216
|
errorType: errorType ?? null,
|
|
197
217
|
isProcessing: false,
|
|
198
218
|
thinkingDuration: elapsedSeconds(startTime),
|
|
199
219
|
}));
|
|
200
220
|
}
|
|
221
|
+
export function filterHistoryForAccessPoints(history, currentAccessPoints) {
|
|
222
|
+
if (currentAccessPoints.size === 0) {
|
|
223
|
+
return history;
|
|
224
|
+
}
|
|
225
|
+
return history.filter((turn) => {
|
|
226
|
+
if (!turn.queriedAccessPoints || turn.queriedAccessPoints.length === 0) {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
return turn.queriedAccessPoints.some((ap) => currentAccessPoints.has(ap));
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
// Access-point names referenced by `p('...')` calls in the SQL, in
|
|
233
|
+
// first-appearance (driving-first) order and de-duplicated. Computed once at
|
|
234
|
+
// generation to persist on the message so downstream consumers (UI, history,
|
|
235
|
+
// integrations) resolve access points structurally instead of re-parsing SQL.
|
|
236
|
+
function orderedAccessPointsFromSql(sql) {
|
|
237
|
+
const ordered = [];
|
|
238
|
+
const seen = new Set();
|
|
239
|
+
for (const match of sql.matchAll(AP_CALL_PATTERN)) {
|
|
240
|
+
const path = match.groups?.pId ?? '';
|
|
241
|
+
const dotIdx = path.lastIndexOf('.');
|
|
242
|
+
const apName = dotIdx === -1 ? path : path.slice(dotIdx + 1);
|
|
243
|
+
if (apName.length > 0 && !seen.has(apName)) {
|
|
244
|
+
seen.add(apName);
|
|
245
|
+
ordered.push(apName);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return ordered;
|
|
249
|
+
}
|
|
201
250
|
function buildTurnFromAssistant(userText, asstMsg) {
|
|
202
251
|
if (asstMsg.sql) {
|
|
203
252
|
const turn = {
|
|
@@ -205,6 +254,9 @@ function buildTurnFromAssistant(userText, asstMsg) {
|
|
|
205
254
|
sql: asstMsg.sql,
|
|
206
255
|
intent: LegendAIQuestionIntent.DATA_QUERY,
|
|
207
256
|
};
|
|
257
|
+
if (asstMsg.queriedAccessPoints.length > 0) {
|
|
258
|
+
turn.queriedAccessPoints = asstMsg.queriedAccessPoints;
|
|
259
|
+
}
|
|
208
260
|
if (asstMsg.error) {
|
|
209
261
|
turn.resultSummary = `ERROR: ${asstMsg.error.slice(0, 200)}`;
|
|
210
262
|
}
|
|
@@ -376,10 +428,10 @@ export function buildExecutionErrorMessage(errStr, services) {
|
|
|
376
428
|
}
|
|
377
429
|
return errParts.join('');
|
|
378
430
|
}
|
|
379
|
-
if (errLower.includes('
|
|
431
|
+
if (errLower.includes('duplicate column')) {
|
|
380
432
|
return [
|
|
381
|
-
'
|
|
382
|
-
'\
|
|
433
|
+
'This join produced two output columns with the same name, which the execution engine cannot return.',
|
|
434
|
+
'\nGive each selected column a unique alias (e.g. `<column> AS <accessPoint>_<column>`) so the joined result stays distinct.',
|
|
383
435
|
].join('');
|
|
384
436
|
}
|
|
385
437
|
if (/can't find a match for function '\w+\(Timestamp/i.test(errStr) &&
|
|
@@ -502,11 +554,8 @@ function buildJoinDiagnosticNote(query, columns, rows) {
|
|
|
502
554
|
const someDataColumns = [];
|
|
503
555
|
for (const col of columns) {
|
|
504
556
|
const allNull = rows.every((row) => {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
return val === null || val === undefined;
|
|
508
|
-
}
|
|
509
|
-
return true;
|
|
557
|
+
const val = row[col];
|
|
558
|
+
return val === null || val === undefined;
|
|
510
559
|
});
|
|
511
560
|
if (allNull) {
|
|
512
561
|
allNullColumns.push(col);
|
|
@@ -517,26 +566,84 @@ function buildJoinDiagnosticNote(query, columns, rows) {
|
|
|
517
566
|
}
|
|
518
567
|
if (allNullColumns.length > 0 &&
|
|
519
568
|
someDataColumns.length > 0 &&
|
|
520
|
-
allNullColumns.length >=
|
|
521
|
-
return (`Note: ${allNullColumns.length} columns (${allNullColumns.slice(0,
|
|
569
|
+
allNullColumns.length >= MIN_ALL_NULL_COLUMNS_FOR_JOIN_HINT) {
|
|
570
|
+
return (`Note: ${allNullColumns.length} columns (${allNullColumns.slice(0, JOIN_HINT_COLUMN_PREVIEW_LIMIT).join(', ')}${allNullColumns.length > JOIN_HINT_COLUMN_PREVIEW_LIMIT ? ', ...' : ''}) ` +
|
|
522
571
|
'returned entirely NULL values. This typically means the joined services do not share ' +
|
|
523
572
|
'overlapping values for the join key. The services may track different records. ' +
|
|
524
573
|
'Try querying each service separately to explore their data independently.');
|
|
525
574
|
}
|
|
526
575
|
return '';
|
|
527
576
|
}
|
|
577
|
+
function createColumnDistribution(column) {
|
|
578
|
+
return {
|
|
579
|
+
column,
|
|
580
|
+
counts: new Map(),
|
|
581
|
+
present: 0,
|
|
582
|
+
numeric: 0,
|
|
583
|
+
min: Number.POSITIVE_INFINITY,
|
|
584
|
+
max: Number.NEGATIVE_INFINITY,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
// Formats one accumulated column distribution into a single line:
|
|
588
|
+
// "(all empty)", a numeric range, or the top values with their counts.
|
|
589
|
+
function formatColumnDistribution(stats) {
|
|
590
|
+
const { column, counts, present, numeric, min, max } = stats;
|
|
591
|
+
if (present === 0) {
|
|
592
|
+
return `- ${column}: (all empty)`;
|
|
593
|
+
}
|
|
594
|
+
if (numeric === present &&
|
|
595
|
+
counts.size > present * MIN_NUMERIC_DISTINCT_RATIO) {
|
|
596
|
+
return `- ${column}: numeric, range ${min}–${max} (${present} values)`;
|
|
597
|
+
}
|
|
598
|
+
const top = Array.from(counts.entries())
|
|
599
|
+
.sort((a, b) => b[1] - a[1])
|
|
600
|
+
.slice(0, MAX_AGGREGATE_VALUES_PER_COLUMN)
|
|
601
|
+
.map(([value, count]) => `${value} (${count})`)
|
|
602
|
+
.join(', ');
|
|
603
|
+
const more = counts.size > MAX_AGGREGATE_VALUES_PER_COLUMN
|
|
604
|
+
? `, … ${counts.size} distinct total`
|
|
605
|
+
: '';
|
|
606
|
+
return `- ${column}: ${top}${more}`;
|
|
607
|
+
}
|
|
608
|
+
export function buildDataAggregatesText(columns, rows) {
|
|
609
|
+
if (rows.length === 0) {
|
|
610
|
+
return 'Total rows: 0';
|
|
611
|
+
}
|
|
612
|
+
const distributions = columns
|
|
613
|
+
.slice(0, MAX_AGGREGATE_COLUMNS)
|
|
614
|
+
.map(createColumnDistribution);
|
|
615
|
+
for (const row of rows) {
|
|
616
|
+
for (const stats of distributions) {
|
|
617
|
+
const raw = row[stats.column];
|
|
618
|
+
if (raw === null || raw === undefined || raw === '') {
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
stats.present += 1;
|
|
622
|
+
const asString = String(raw);
|
|
623
|
+
stats.counts.set(asString, (stats.counts.get(asString) ?? 0) + 1);
|
|
624
|
+
const asNumber = typeof raw === 'number' ? raw : Number(asString);
|
|
625
|
+
if (asString.trim() !== '' && !Number.isNaN(asNumber)) {
|
|
626
|
+
stats.numeric += 1;
|
|
627
|
+
stats.min = Math.min(stats.min, asNumber);
|
|
628
|
+
stats.max = Math.max(stats.max, asNumber);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return [
|
|
633
|
+
`Total rows: ${rows.length}`,
|
|
634
|
+
'Column value distributions over ALL rows (value: count):',
|
|
635
|
+
...distributions.map(formatColumnDistribution),
|
|
636
|
+
].join('\n');
|
|
637
|
+
}
|
|
528
638
|
function buildDeterministicResultSummary(question, query, columns, rows) {
|
|
529
639
|
const selectedColumns = columns.length === 0 ? 'none' : columns.join(', ');
|
|
530
640
|
const rowCount = rows.length;
|
|
531
641
|
const previewRows = rows
|
|
532
642
|
.slice(0, ANALYSIS_PREVIEW_ROW_LIMIT)
|
|
533
643
|
.map((row, index) => {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
return `${index + 1}. ${formattedEntries.join(', ')}`;
|
|
538
|
-
}
|
|
539
|
-
return `${index + 1}. ${formatPreviewValue(row)}`;
|
|
644
|
+
const entries = Object.entries(row).slice(0, 4);
|
|
645
|
+
const formattedEntries = entries.map(([key, value]) => `${key}: ${formatPreviewValue(value)}`);
|
|
646
|
+
return `${index + 1}. ${formattedEntries.join(', ')}`;
|
|
540
647
|
})
|
|
541
648
|
.join('\n');
|
|
542
649
|
const coverageNote = buildQuestionCoverageNote(question, columns);
|
|
@@ -610,7 +717,9 @@ function handleNonSqlPass(setMessages, nonSqlPassAttempts, attempt, maxAttempts)
|
|
|
610
717
|
return 'continue';
|
|
611
718
|
}
|
|
612
719
|
function finalizeJudgeAttempt(currentSql, previousSql, attempt, maxAttempts, setMessages) {
|
|
613
|
-
if (currentSql !==
|
|
720
|
+
if (normalizeSqlForComparison(currentSql) !==
|
|
721
|
+
normalizeSqlForComparison(previousSql) &&
|
|
722
|
+
attempt < maxAttempts) {
|
|
614
723
|
return undefined;
|
|
615
724
|
}
|
|
616
725
|
addThinkingStep(setMessages, isLikelySqlQuery(currentSql)
|
|
@@ -780,7 +889,7 @@ export async function analyzeOrchestratorResults(question, query, execResult, me
|
|
|
780
889
|
updateLastAssistant(setMessages, () => ({
|
|
781
890
|
isProcessing: true,
|
|
782
891
|
}));
|
|
783
|
-
const analysis = await withTimeout(plugin.analyzeQueryResults(question, query, execResult.columns, execResult.rows, metadata, config), ANALYSIS_TIMEOUT_MS);
|
|
892
|
+
const analysis = await withTimeout(plugin.analyzeQueryResults(question, query, execResult.columns, execResult.rows.slice(0, MAX_ANALYSIS_ROWS), metadata, config, execResult.rows.length, buildDataAggregatesText(execResult.columns, execResult.rows)), ANALYSIS_TIMEOUT_MS);
|
|
784
893
|
addThinkingStep(setMessages, 'Verifying answer coverage...');
|
|
785
894
|
if (analysis) {
|
|
786
895
|
const coverageNote = buildQuestionCoverageNote(question, execResult.columns);
|
|
@@ -850,32 +959,171 @@ async function retryWithAlternateRoot(question, alternateRoot, resolvedEntities,
|
|
|
850
959
|
return false;
|
|
851
960
|
}
|
|
852
961
|
}
|
|
853
|
-
async function
|
|
962
|
+
async function attemptProbedRegeneration(question, legendQuery, resolvedEntities, orchestratorOptions, metadata, context, options) {
|
|
854
963
|
const { startTime, modelContext } = options;
|
|
964
|
+
if (!modelContext) {
|
|
965
|
+
return false;
|
|
966
|
+
}
|
|
967
|
+
const { dataProductCoordinates, pureExecutionContext } = orchestratorOptions;
|
|
855
968
|
const { config, plugin, setMessages } = context;
|
|
969
|
+
const rootEntity = modelContext.entities.find((entity) => entity.path === resolvedEntities.rootEntity);
|
|
970
|
+
if (!rootEntity) {
|
|
971
|
+
return false;
|
|
972
|
+
}
|
|
973
|
+
const columns = extractFilteredColumns(legendQuery).filter((column) => rootEntity.properties.some((prop) => prop.name === column && !prop.type.includes('::')));
|
|
974
|
+
if (columns.length === 0) {
|
|
975
|
+
return false;
|
|
976
|
+
}
|
|
977
|
+
addThinkingStep(setMessages, 'Re-grounding with actual column values...');
|
|
978
|
+
const probeResults = await Promise.all(columns.slice(0, MAX_RECOVERED_FILTER_COLUMNS).map(async (column) => {
|
|
979
|
+
try {
|
|
980
|
+
const probe = await withTimeout(plugin.executePureQuery(`${resolvedEntities.rootEntity}.all()->project([col(x|$x.${column}, '${column}')])->distinct()->take(${DISTINCT_PROBE_ROW_LIMIT})`, pureExecutionContext, dataProductCoordinates, config), ORCHESTRATOR_GENERATION_TIMEOUT_MS);
|
|
981
|
+
const values = (probe?.rows ?? [])
|
|
982
|
+
.map((row) => row[column])
|
|
983
|
+
.filter(isNonNullable)
|
|
984
|
+
.map(String);
|
|
985
|
+
return values.length > 0
|
|
986
|
+
? { column, values: Array.from(new Set(values)) }
|
|
987
|
+
: undefined;
|
|
988
|
+
}
|
|
989
|
+
catch (probeError) {
|
|
990
|
+
assertErrorThrown(probeError);
|
|
991
|
+
addThinkingStep(setMessages, `Could not probe values for "${column}"`);
|
|
992
|
+
return undefined;
|
|
993
|
+
}
|
|
994
|
+
}));
|
|
995
|
+
const probed = new Map();
|
|
996
|
+
for (const entry of probeResults) {
|
|
997
|
+
if (entry !== undefined) {
|
|
998
|
+
probed.set(entry.column, entry.values);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
if (probed.size === 0) {
|
|
1002
|
+
return false;
|
|
1003
|
+
}
|
|
1004
|
+
const base = buildEnrichedBusinessContext(question, resolvedEntities.rootEntity, resolvedEntities.relatedEntities, modelContext);
|
|
1005
|
+
const augmentedContext = {
|
|
1006
|
+
...base,
|
|
1007
|
+
businessContextMatch: {
|
|
1008
|
+
...base.businessContextMatch,
|
|
1009
|
+
additionalNlModelContext: [
|
|
1010
|
+
...(base.businessContextMatch?.additionalNlModelContext ?? []),
|
|
1011
|
+
...buildProbedValueHints(probed),
|
|
1012
|
+
],
|
|
1013
|
+
},
|
|
1014
|
+
};
|
|
1015
|
+
try {
|
|
1016
|
+
const response = await withTimeout(plugin.generateQueryViaOrchestrator({
|
|
1017
|
+
user_question: question,
|
|
1018
|
+
semantic_search_resolution_details: {
|
|
1019
|
+
data_product_coordinates: dataProductCoordinates,
|
|
1020
|
+
root_entity: resolvedEntities.rootEntity,
|
|
1021
|
+
related_entities: resolvedEntities.relatedEntities,
|
|
1022
|
+
enriched_business_context: augmentedContext,
|
|
1023
|
+
},
|
|
1024
|
+
}, config), ORCHESTRATOR_GENERATION_TIMEOUT_MS);
|
|
1025
|
+
if (!response) {
|
|
1026
|
+
return false;
|
|
1027
|
+
}
|
|
1028
|
+
const safeQuery = ensurePureSafetyLimit(response.legend_query);
|
|
1029
|
+
updateLastAssistant(setMessages, () => ({
|
|
1030
|
+
sql: safeQuery,
|
|
1031
|
+
isExecuting: true,
|
|
1032
|
+
}));
|
|
1033
|
+
const result = await executePureQueryAndReport(safeQuery, pureExecutionContext, dataProductCoordinates, config, plugin, setMessages, startTime);
|
|
1034
|
+
if (result && result.rows.length > 0) {
|
|
1035
|
+
await analyzeOrchestratorResults(question, safeQuery, result, metadata, context, startTime);
|
|
1036
|
+
return true;
|
|
1037
|
+
}
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
catch (regenerateError) {
|
|
1041
|
+
assertErrorThrown(regenerateError);
|
|
1042
|
+
addThinkingStep(setMessages, `Re-grounded retry failed: ${regenerateError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
1043
|
+
return false;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
// Retries the query with case-insensitive (relaxed) string filters. Returns
|
|
1047
|
+
// true when the relaxed query returned rows and its results were reported.
|
|
1048
|
+
async function attemptRelaxedFilterRetry(question, legendQuery, orchestratorOptions, metadata, context, startTime) {
|
|
1049
|
+
const { config, plugin, setMessages } = context;
|
|
1050
|
+
const { dataProductCoordinates, pureExecutionContext } = orchestratorOptions;
|
|
1051
|
+
const relaxedQuery = relaxExactStringFilters(legendQuery);
|
|
1052
|
+
if (relaxedQuery === legendQuery) {
|
|
1053
|
+
return false;
|
|
1054
|
+
}
|
|
1055
|
+
addThinkingStep(setMessages, 'Retrying with relaxed (case-insensitive) filters...');
|
|
1056
|
+
try {
|
|
1057
|
+
const safeRelaxed = ensurePureSafetyLimit(relaxedQuery);
|
|
1058
|
+
updateLastAssistant(setMessages, () => ({
|
|
1059
|
+
sql: safeRelaxed,
|
|
1060
|
+
isExecuting: true,
|
|
1061
|
+
}));
|
|
1062
|
+
const relaxedResult = await executePureQueryAndReport(safeRelaxed, pureExecutionContext, dataProductCoordinates, config, plugin, setMessages, startTime);
|
|
1063
|
+
if (relaxedResult && relaxedResult.rows.length > 0) {
|
|
1064
|
+
await analyzeOrchestratorResults(question, relaxedQuery, relaxedResult, metadata, context, startTime);
|
|
1065
|
+
return true;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
catch (relaxError) {
|
|
1069
|
+
assertErrorThrown(relaxError);
|
|
1070
|
+
addThinkingStep(setMessages, `Relaxed-filter retry failed: ${relaxError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
1071
|
+
}
|
|
1072
|
+
return false;
|
|
1073
|
+
}
|
|
1074
|
+
async function handleEmptyOrchestratorResults(question, legendQuery, orchestratorOptions, metadata, resolvedEntities, context, options) {
|
|
1075
|
+
const { startTime, modelContext } = options;
|
|
1076
|
+
const { setMessages } = context;
|
|
1077
|
+
if (await attemptRelaxedFilterRetry(question, legendQuery, orchestratorOptions, metadata, context, startTime)) {
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
const regenerated = await attemptProbedRegeneration(question, legendQuery, resolvedEntities, orchestratorOptions, metadata, context, { startTime, ...(modelContext === undefined ? {} : { modelContext }) });
|
|
1081
|
+
if (regenerated) {
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
const triedRoots = new Set([resolvedEntities.rootEntity]);
|
|
856
1085
|
if (resolvedEntities.relatedEntities.length > 0) {
|
|
857
1086
|
const alternateRoot = modelContext
|
|
858
1087
|
? findBestAlternateRoot(resolvedEntities.rootEntity, resolvedEntities.relatedEntities, modelContext)
|
|
859
1088
|
: resolvedEntities.relatedEntities[0];
|
|
860
1089
|
if (alternateRoot) {
|
|
1090
|
+
triedRoots.add(alternateRoot);
|
|
861
1091
|
const succeeded = await retryWithAlternateRoot(question, alternateRoot, resolvedEntities, orchestratorOptions, metadata, context, { startTime, ...(modelContext === undefined ? {} : { modelContext }) });
|
|
862
1092
|
if (succeeded) {
|
|
863
1093
|
return;
|
|
864
1094
|
}
|
|
865
1095
|
}
|
|
866
1096
|
}
|
|
867
|
-
addThinkingStep(setMessages, 'No results
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
1097
|
+
addThinkingStep(setMessages, 'No results found for this question.');
|
|
1098
|
+
completeThinkingSteps(setMessages);
|
|
1099
|
+
updateLastAssistant(setMessages, () => ({
|
|
1100
|
+
textAnswer: buildUnanswerableMessage(),
|
|
1101
|
+
suggestedQueries: [],
|
|
1102
|
+
isProcessing: false,
|
|
1103
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
1104
|
+
}));
|
|
1105
|
+
attachAlternateRootFallback(question, resolvedEntities, triedRoots, modelContext, setMessages);
|
|
1106
|
+
}
|
|
1107
|
+
// Offers a "try a different root entity" fallback action when a not-yet-tried,
|
|
1108
|
+
// positively-ranked root exists for the question.
|
|
1109
|
+
function attachAlternateRootFallback(question, resolvedEntities, triedRoots, modelContext, setMessages) {
|
|
1110
|
+
const nextRoot = modelContext
|
|
1111
|
+
? rankEntities(question, modelContext).find((ranked) => !triedRoots.has(ranked.entity.path) && ranked.score > 0)?.entity
|
|
1112
|
+
: undefined;
|
|
1113
|
+
if (!nextRoot) {
|
|
1114
|
+
return;
|
|
878
1115
|
}
|
|
1116
|
+
const nextRelated = resolvedEntities.relatedEntities.filter((path) => path !== nextRoot.path);
|
|
1117
|
+
updateLastAssistant(setMessages, () => ({
|
|
1118
|
+
fallbackAction: {
|
|
1119
|
+
label: `Tried ${extractElementNameFromPath(resolvedEntities.rootEntity)} as the root \u2014 try ${nextRoot.name} instead`,
|
|
1120
|
+
actionId: LEGEND_AI_ALTERNATE_ROOT_ACTION_ID,
|
|
1121
|
+
resolvedEntities: {
|
|
1122
|
+
rootEntity: nextRoot.path,
|
|
1123
|
+
relatedEntities: nextRelated,
|
|
1124
|
+
},
|
|
1125
|
+
},
|
|
1126
|
+
}));
|
|
879
1127
|
}
|
|
880
1128
|
async function resolveOrchestrationEntities(question, dataProductCoordinates, context, pureExecutionContext, preResolvedEntities, modelContext) {
|
|
881
1129
|
const { config, plugin, setMessages } = context;
|
|
@@ -892,14 +1140,12 @@ function extractUserErrorFromMessage(errorMessage) {
|
|
|
892
1140
|
const marker = 'with error:';
|
|
893
1141
|
const idx = errorMessage.toLowerCase().indexOf(marker);
|
|
894
1142
|
if (idx !== -1) {
|
|
895
|
-
const
|
|
896
|
-
const newlineIdx = after.indexOf('\n');
|
|
897
|
-
const reason = (newlineIdx === -1 ? after : after.slice(0, newlineIdx)).trim();
|
|
1143
|
+
const reason = errorMessage.slice(idx + marker.length).trim();
|
|
898
1144
|
if (reason.length > 0) {
|
|
899
|
-
return reason.slice(0,
|
|
1145
|
+
return reason.slice(0, MAX_DISPLAYED_ERROR_LENGTH);
|
|
900
1146
|
}
|
|
901
1147
|
}
|
|
902
|
-
return errorMessage.slice(0,
|
|
1148
|
+
return errorMessage.slice(0, MAX_DISPLAYED_ERROR_LENGTH);
|
|
903
1149
|
}
|
|
904
1150
|
async function handleOrchestratorError(error, startTime, question, metadata, context) {
|
|
905
1151
|
const { config, plugin, setMessages } = context;
|
|
@@ -926,7 +1172,38 @@ async function handleOrchestratorError(error, startTime, question, metadata, con
|
|
|
926
1172
|
assertErrorThrown(fallbackError);
|
|
927
1173
|
addThinkingStep(setMessages, `Fallback guidance failed: ${fallbackError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
928
1174
|
}
|
|
929
|
-
finishWithThinkingError(setMessages, error.message, startTime, orchErrorType);
|
|
1175
|
+
finishWithThinkingError(setMessages, extractUserErrorFromMessage(error.message), startTime, orchErrorType);
|
|
1176
|
+
}
|
|
1177
|
+
// Analyzes a non-empty orchestrator result (or runs empty-result recovery),
|
|
1178
|
+
// attaching the model-approach overview when available.
|
|
1179
|
+
async function reportOrchestratorExecutionResult(params, context) {
|
|
1180
|
+
const { question, safeQuery, execResult, orchestratorOptions, metadata, resolvedEntities, startTime, modelContext, } = params;
|
|
1181
|
+
const { setMessages } = context;
|
|
1182
|
+
try {
|
|
1183
|
+
if (execResult.rows.length > 0) {
|
|
1184
|
+
await analyzeOrchestratorResults(question, safeQuery, execResult, metadata, context, startTime);
|
|
1185
|
+
if (modelContext) {
|
|
1186
|
+
const approachText = buildDataQueryApproachText(question, modelContext, resolvedEntities.rootEntity);
|
|
1187
|
+
if (approachText) {
|
|
1188
|
+
attachMetadataOverview(setMessages, approachText);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
else {
|
|
1193
|
+
await handleEmptyOrchestratorResults(question, safeQuery, orchestratorOptions, metadata, resolvedEntities, context, { startTime, ...(modelContext === undefined ? {} : { modelContext }) });
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
catch (analysisError) {
|
|
1197
|
+
assertErrorThrown(analysisError);
|
|
1198
|
+
addThinkingStep(setMessages, `Result analysis failed: ${analysisError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
1199
|
+
}
|
|
1200
|
+
finally {
|
|
1201
|
+
completeThinkingSteps(setMessages);
|
|
1202
|
+
updateLastAssistant(setMessages, () => ({
|
|
1203
|
+
isProcessing: false,
|
|
1204
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
1205
|
+
}));
|
|
1206
|
+
}
|
|
930
1207
|
}
|
|
931
1208
|
export async function processQuestionViaOrchestrator(question, dataProductCoordinates, metadata, context, pureExecutionContext, preResolvedEntities, modelContext) {
|
|
932
1209
|
const { config, plugin, setMessages } = context;
|
|
@@ -976,28 +1253,16 @@ export async function processQuestionViaOrchestrator(question, dataProductCoordi
|
|
|
976
1253
|
if (!execResult) {
|
|
977
1254
|
return;
|
|
978
1255
|
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
}
|
|
990
|
-
catch (analysisError) {
|
|
991
|
-
assertErrorThrown(analysisError);
|
|
992
|
-
addThinkingStep(setMessages, `Result analysis failed: ${analysisError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
993
|
-
}
|
|
994
|
-
finally {
|
|
995
|
-
completeThinkingSteps(setMessages);
|
|
996
|
-
updateLastAssistant(setMessages, () => ({
|
|
997
|
-
isProcessing: false,
|
|
998
|
-
thinkingDuration: elapsedSeconds(startTime),
|
|
999
|
-
}));
|
|
1000
|
-
}
|
|
1256
|
+
await reportOrchestratorExecutionResult({
|
|
1257
|
+
question,
|
|
1258
|
+
safeQuery,
|
|
1259
|
+
execResult,
|
|
1260
|
+
orchestratorOptions: { dataProductCoordinates, pureExecutionContext },
|
|
1261
|
+
metadata,
|
|
1262
|
+
resolvedEntities,
|
|
1263
|
+
startTime,
|
|
1264
|
+
...(modelContext === undefined ? {} : { modelContext }),
|
|
1265
|
+
}, context);
|
|
1001
1266
|
}
|
|
1002
1267
|
catch (error) {
|
|
1003
1268
|
assertErrorThrown(error);
|
|
@@ -1012,97 +1277,14 @@ export function cleanLlmSqlResponse(raw) {
|
|
|
1012
1277
|
.replace(/;\s*$/, '')
|
|
1013
1278
|
.trim();
|
|
1014
1279
|
}
|
|
1280
|
+
function normalizeSqlForComparison(sql) {
|
|
1281
|
+
return sql.replaceAll(/\s+/gu, ' ').trim().toLowerCase();
|
|
1282
|
+
}
|
|
1015
1283
|
export function isValidSqlCorrection(trimmed, currentSql) {
|
|
1016
1284
|
return (trimmed.length > 0 &&
|
|
1017
1285
|
trimmed.toLowerCase().startsWith('select') &&
|
|
1018
1286
|
trimmed !== currentSql);
|
|
1019
1287
|
}
|
|
1020
|
-
export function sanitizeJoinOrderBy(sql) {
|
|
1021
|
-
if (!JOIN_PATTERN.test(sql)) {
|
|
1022
|
-
return sql;
|
|
1023
|
-
}
|
|
1024
|
-
const parts = sql.split(ORDER_BY_SPLIT);
|
|
1025
|
-
if (parts.length < 2) {
|
|
1026
|
-
return sql;
|
|
1027
|
-
}
|
|
1028
|
-
const beforeOrderBy = parts[0] ?? '';
|
|
1029
|
-
const afterOrderBy = parts.slice(1).join('ORDER BY').replace(/^\s+/, '');
|
|
1030
|
-
const selectAliases = new Map();
|
|
1031
|
-
for (const m of beforeOrderBy.matchAll(SELECT_ALIAS_PATTERN)) {
|
|
1032
|
-
const tableAlias = (m.groups?.tbl ?? '').toLowerCase();
|
|
1033
|
-
const colName = (m.groups?.col ?? '').toLowerCase();
|
|
1034
|
-
const asAlias = m.groups?.qAlias ?? m.groups?.uAlias ?? '';
|
|
1035
|
-
selectAliases.set(`${tableAlias}.${colName}`, asAlias);
|
|
1036
|
-
}
|
|
1037
|
-
if (selectAliases.size === 0) {
|
|
1038
|
-
return sql;
|
|
1039
|
-
}
|
|
1040
|
-
// Build a lookup of the original alias.col text → its SELECT alias replacement.
|
|
1041
|
-
// matchAll exposes typed `.groups`, so no cast is needed.
|
|
1042
|
-
const replacements = new Map();
|
|
1043
|
-
for (const m of afterOrderBy.matchAll(ALIAS_DOT_COL_PATTERN)) {
|
|
1044
|
-
const tbl = (m.groups?.tbl ?? '').toLowerCase();
|
|
1045
|
-
const col = (m.groups?.col ?? '').toLowerCase();
|
|
1046
|
-
const alias = selectAliases.get(`${tbl}.${col}`);
|
|
1047
|
-
if (alias) {
|
|
1048
|
-
replacements.set(m[0], `"${alias}"`);
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
if (replacements.size === 0) {
|
|
1052
|
-
return sql;
|
|
1053
|
-
}
|
|
1054
|
-
const rewritten = afterOrderBy.replaceAll(ALIAS_DOT_COL_PATTERN, (match) => replacements.get(match) ?? match);
|
|
1055
|
-
if (rewritten === afterOrderBy) {
|
|
1056
|
-
return sql;
|
|
1057
|
-
}
|
|
1058
|
-
return `${beforeOrderBy}ORDER BY ${rewritten}`;
|
|
1059
|
-
}
|
|
1060
|
-
export function sanitizeJoinSameKeyColumns(sql, services) {
|
|
1061
|
-
if (!JOIN_PATTERN.test(sql)) {
|
|
1062
|
-
return sql;
|
|
1063
|
-
}
|
|
1064
|
-
const joinRegex = /\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;
|
|
1065
|
-
let result = sql;
|
|
1066
|
-
let match;
|
|
1067
|
-
while ((match = joinRegex.exec(sql)) !== null) {
|
|
1068
|
-
const pId = match.groups?.pId ?? '';
|
|
1069
|
-
const rightAlias = match.groups?.rAlias ?? '';
|
|
1070
|
-
const onClause = (match.groups?.onClause ?? '').slice(0, 500);
|
|
1071
|
-
const sameKeyMatch = /(?<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(onClause);
|
|
1072
|
-
if (!sameKeyMatch) {
|
|
1073
|
-
continue;
|
|
1074
|
-
}
|
|
1075
|
-
const leftCol = sameKeyMatch.groups?.lCol ?? '';
|
|
1076
|
-
const rightCol = sameKeyMatch.groups?.rCol ?? '';
|
|
1077
|
-
const matchedRightAlias = sameKeyMatch.groups?.rAl ?? '';
|
|
1078
|
-
if (leftCol.toLowerCase() !== rightCol.toLowerCase() ||
|
|
1079
|
-
matchedRightAlias.toLowerCase() !== rightAlias.toLowerCase()) {
|
|
1080
|
-
continue;
|
|
1081
|
-
}
|
|
1082
|
-
const renamedKey = `${rightAlias}_${rightCol}`;
|
|
1083
|
-
const columnList = buildSubqueryColumnList(pId, rightCol, renamedKey, services);
|
|
1084
|
-
const originalFragment = `p('${pId}') AS ${rightAlias}`;
|
|
1085
|
-
const subqueryFragment = `(SELECT ${columnList} FROM p('${pId}')) AS ${rightAlias}`;
|
|
1086
|
-
const oldOnRef = `${rightAlias}."${rightCol}"`;
|
|
1087
|
-
const newOnRef = `${rightAlias}."${renamedKey}"`;
|
|
1088
|
-
result = result.replace(originalFragment, subqueryFragment);
|
|
1089
|
-
result = result.replaceAll(oldOnRef, newOnRef);
|
|
1090
|
-
}
|
|
1091
|
-
return result;
|
|
1092
|
-
}
|
|
1093
|
-
function buildSubqueryColumnList(pId, originalKey, renamedKey, services) {
|
|
1094
|
-
const svc = services?.find((s) => s.dataProductPath &&
|
|
1095
|
-
s.pattern &&
|
|
1096
|
-
pId === `${s.dataProductPath}.${s.pattern.replace(/^\//, '')}`);
|
|
1097
|
-
if (!svc || svc.columns.length === 0) {
|
|
1098
|
-
return `"${originalKey}" AS "${renamedKey}", *`;
|
|
1099
|
-
}
|
|
1100
|
-
return svc.columns
|
|
1101
|
-
.map((c) => c.name.toLowerCase() === originalKey.toLowerCase()
|
|
1102
|
-
? `"${c.name}" AS "${renamedKey}"`
|
|
1103
|
-
: `"${c.name}"`)
|
|
1104
|
-
.join(', ');
|
|
1105
|
-
}
|
|
1106
1288
|
export function sanitizeLiteralColumns(sql) {
|
|
1107
1289
|
if (!UNION_ALL_PATTERN.test(sql)) {
|
|
1108
1290
|
return sql;
|
|
@@ -1335,7 +1517,10 @@ export function ensureSafeLimit(sql, limit = DEFAULT_SAFETY_LIMIT) {
|
|
|
1335
1517
|
return `${sql.trimEnd()}\nLIMIT ${limit}`;
|
|
1336
1518
|
}
|
|
1337
1519
|
function prepareSafeSql(sql, services) {
|
|
1338
|
-
const
|
|
1520
|
+
const joined = sanitizeLiteralColumns(sanitizeJoinSameKeyColumns(sanitizeJoinOrderBy(sanitizeJoinDuplicateColumns(sql, services)), services));
|
|
1521
|
+
const wrapped = wrapBareJoinAccessPoints(joined, services);
|
|
1522
|
+
const bounded = boundCrossAccessPointJoinDrivingSide(wrapped);
|
|
1523
|
+
const safeSql = ensureSafeLimit(ensureDateParameters(bounded, services));
|
|
1339
1524
|
const unsupported = detectUnsupportedEnginePattern(safeSql);
|
|
1340
1525
|
if (unsupported) {
|
|
1341
1526
|
throw new LegendAIUnsupportedEngineShapeError(unsupported.hint);
|
|
@@ -1353,7 +1538,11 @@ async function executeSqlForServices(sql, services, dataProductCoordinates, plug
|
|
|
1353
1538
|
const safeSql = prepareSafeSql(sql, services);
|
|
1354
1539
|
const isAccessPoint = services.some((s) => s.sourceType === TDSServiceSourceType.ACCESS_POINT);
|
|
1355
1540
|
if (isAccessPoint && dataProductCoordinates) {
|
|
1356
|
-
|
|
1541
|
+
const result = await withTimeout(plugin.executeLakehouseSql(safeSql, dataProductCoordinates, config), AP_SQL_EXECUTION_TIMEOUT_MS);
|
|
1542
|
+
if (result === undefined) {
|
|
1543
|
+
throw new LegendAIExecutionTimeoutError(AP_SQL_EXECUTION_TIMEOUT_MESSAGE);
|
|
1544
|
+
}
|
|
1545
|
+
return result;
|
|
1357
1546
|
}
|
|
1358
1547
|
return plugin.executeSql(safeSql, config);
|
|
1359
1548
|
}
|
|
@@ -1367,6 +1556,9 @@ async function executeSqlWithRetries(initialSql, question, services, coordinates
|
|
|
1367
1556
|
}
|
|
1368
1557
|
catch (executeError) {
|
|
1369
1558
|
assertErrorThrown(executeError);
|
|
1559
|
+
if (executeError instanceof LegendAIExecutionTimeoutError) {
|
|
1560
|
+
return { sql: currentSql, error: executeError.message };
|
|
1561
|
+
}
|
|
1370
1562
|
if (attempt >= DEFAULT_MAX_EXECUTION_RETRIES) {
|
|
1371
1563
|
return { sql: currentSql, error: executeError.message };
|
|
1372
1564
|
}
|
|
@@ -1466,7 +1658,10 @@ function buildZeroRowMessage(services) {
|
|
|
1466
1658
|
const names = withFailedExtraction.map((s) => `**${s.title}**`).join(', ');
|
|
1467
1659
|
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.`;
|
|
1468
1660
|
}
|
|
1469
|
-
return
|
|
1661
|
+
return buildUnanswerableMessage();
|
|
1662
|
+
}
|
|
1663
|
+
function buildUnanswerableMessage() {
|
|
1664
|
+
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.`;
|
|
1470
1665
|
}
|
|
1471
1666
|
function handleSqlGenerationFailure(setMessages, startTime, hasOrchestratorFallback, orchestratorMessage, errorMessage, errorType, suggestedQueries) {
|
|
1472
1667
|
const suggestions = suggestedQueries ?? [];
|
|
@@ -1493,8 +1688,8 @@ function handleSqlGenerationFailure(setMessages, startTime, hasOrchestratorFallb
|
|
|
1493
1688
|
}
|
|
1494
1689
|
}
|
|
1495
1690
|
async function reportQueryResults(report, metadata, context, startTime, hasOrchestratorFallback) {
|
|
1496
|
-
const { currentSql, sqlResult, question, services } = report;
|
|
1497
|
-
const { setMessages } = context;
|
|
1691
|
+
const { currentSql, sqlResult, question, services, allAccessPoints } = report;
|
|
1692
|
+
const { config, plugin, setMessages } = context;
|
|
1498
1693
|
if (sqlResult.rows.length > 0) {
|
|
1499
1694
|
const columns = deduplicateColumns(sqlResult.columns);
|
|
1500
1695
|
const rows = sqlResult.rows;
|
|
@@ -1529,7 +1724,20 @@ async function reportQueryResults(report, metadata, context, startTime, hasOrche
|
|
|
1529
1724
|
}
|
|
1530
1725
|
else {
|
|
1531
1726
|
addThinkingStep(setMessages, 'Query returned 0 rows after correction attempts.');
|
|
1727
|
+
const joinExplanation = buildCrossJoinZeroRowExplanation(currentSql, services);
|
|
1728
|
+
const joinSuggestions = joinExplanation === undefined
|
|
1729
|
+
? []
|
|
1730
|
+
: buildJoinablePairSuggestions(services, allAccessPoints ?? services);
|
|
1731
|
+
const llmAnalysis = await withTimeout(plugin
|
|
1732
|
+
.buildNoResultsFallback(question, currentSql, metadata, config, joinExplanation)
|
|
1733
|
+
.catch((error) => {
|
|
1734
|
+
assertErrorThrown(error);
|
|
1735
|
+
return undefined;
|
|
1736
|
+
}), ANALYSIS_TIMEOUT_MS);
|
|
1532
1737
|
completeThinkingSteps(setMessages);
|
|
1738
|
+
const suggestions = joinSuggestions.length > 0
|
|
1739
|
+
? joinSuggestions
|
|
1740
|
+
: (llmAnalysis?.suggestedQueries ?? []);
|
|
1533
1741
|
const fallback = hasOrchestratorFallback
|
|
1534
1742
|
? {
|
|
1535
1743
|
fallbackAction: {
|
|
@@ -1539,7 +1747,10 @@ async function reportQueryResults(report, metadata, context, startTime, hasOrche
|
|
|
1539
1747
|
}
|
|
1540
1748
|
: {};
|
|
1541
1749
|
updateLastAssistant(setMessages, () => ({
|
|
1542
|
-
textAnswer:
|
|
1750
|
+
textAnswer: llmAnalysis?.summary ??
|
|
1751
|
+
joinExplanation ??
|
|
1752
|
+
buildZeroRowMessage(services),
|
|
1753
|
+
...(suggestions.length > 0 ? { suggestedQueries: suggestions } : {}),
|
|
1543
1754
|
...fallback,
|
|
1544
1755
|
isProcessing: false,
|
|
1545
1756
|
isExecuting: false,
|
|
@@ -1547,7 +1758,8 @@ async function reportQueryResults(report, metadata, context, startTime, hasOrche
|
|
|
1547
1758
|
}));
|
|
1548
1759
|
}
|
|
1549
1760
|
}
|
|
1550
|
-
|
|
1761
|
+
const DEFAULT_MAX_SUPPLEMENTED_SERVICES = 4;
|
|
1762
|
+
export function supplementMissingCoverage(question, selected, allServices, maxTotal = DEFAULT_MAX_SUPPLEMENTED_SERVICES) {
|
|
1551
1763
|
if (selected.length >= maxTotal || allServices.length <= selected.length) {
|
|
1552
1764
|
return selected;
|
|
1553
1765
|
}
|
|
@@ -1710,10 +1922,289 @@ async function selectBestServices(question, services, context) {
|
|
|
1710
1922
|
return candidates;
|
|
1711
1923
|
}
|
|
1712
1924
|
}
|
|
1925
|
+
const EQUALITY_PREDICATE_PATTERN = /"(?<col>[^"]+)"\s*=\s*'(?<val>[^']*(?:''[^']*)*)'/gu;
|
|
1926
|
+
export const IN_PREDICATE_PATTERN = /"(?<col>[^"]+)"\s+in\s*\((?<list>[^)]+)\)/giu;
|
|
1927
|
+
export const IN_LIST_LITERAL_PATTERN = /'(?<val>[^']*(?:''[^']*)*)'/gu;
|
|
1928
|
+
const MIN_FUZZY_LITERAL_LENGTH = 3;
|
|
1929
|
+
function normalizeFilterLiteral(value) {
|
|
1930
|
+
return value.replaceAll(/[^a-z0-9]/giu, '').toUpperCase();
|
|
1931
|
+
}
|
|
1932
|
+
function escapeSqlStringLiteral(value) {
|
|
1933
|
+
return value.replaceAll("'", "''");
|
|
1934
|
+
}
|
|
1935
|
+
export function extractFromClause(sql) {
|
|
1936
|
+
const fromMatch = /\bfrom\b/iu.exec(sql);
|
|
1937
|
+
if (!fromMatch) {
|
|
1938
|
+
return undefined;
|
|
1939
|
+
}
|
|
1940
|
+
const rest = sql.slice(fromMatch.index);
|
|
1941
|
+
const afterFrom = rest.slice('from'.length);
|
|
1942
|
+
const boundary = /\b(?:where|group\s+by|order\s+by|having|limit)\b/iu.exec(afterFrom);
|
|
1943
|
+
return (boundary ? rest.slice(0, 'from'.length + boundary.index) : rest).trim();
|
|
1944
|
+
}
|
|
1945
|
+
// Extracts the WHERE clause segment (from the first `WHERE` up to the next
|
|
1946
|
+
// top-level clause boundary), so filter self-healing only rewrites WHERE
|
|
1947
|
+
// predicates — never JOIN ON keys or HAVING literals.
|
|
1948
|
+
export function extractWhereClause(sql) {
|
|
1949
|
+
const whereMatch = /\bwhere\b/iu.exec(sql);
|
|
1950
|
+
if (!whereMatch) {
|
|
1951
|
+
return undefined;
|
|
1952
|
+
}
|
|
1953
|
+
const rest = sql.slice(whereMatch.index);
|
|
1954
|
+
const afterWhere = rest.slice('where'.length);
|
|
1955
|
+
const boundary = /\b(?:group\s+by|order\s+by|having|limit|window|qualify)\b/iu.exec(afterWhere);
|
|
1956
|
+
return (boundary ? rest.slice(0, 'where'.length + boundary.index) : rest).trim();
|
|
1957
|
+
}
|
|
1958
|
+
export function resolveFilterLiteral(literal, values) {
|
|
1959
|
+
if (values.includes(literal)) {
|
|
1960
|
+
return undefined;
|
|
1961
|
+
}
|
|
1962
|
+
const caseInsensitive = Array.from(new Set(values.filter((v) => v.toLowerCase() === literal.toLowerCase())));
|
|
1963
|
+
if (caseInsensitive.length === 1) {
|
|
1964
|
+
return caseInsensitive[0];
|
|
1965
|
+
}
|
|
1966
|
+
const normalizedLiteral = normalizeFilterLiteral(literal);
|
|
1967
|
+
if (normalizedLiteral.length < MIN_FUZZY_LITERAL_LENGTH) {
|
|
1968
|
+
return undefined;
|
|
1969
|
+
}
|
|
1970
|
+
const normalizedExact = Array.from(new Set(values.filter((v) => normalizeFilterLiteral(v) === normalizedLiteral)));
|
|
1971
|
+
if (normalizedExact.length === 1) {
|
|
1972
|
+
return normalizedExact[0];
|
|
1973
|
+
}
|
|
1974
|
+
const prefixMatches = Array.from(new Set(values.filter((v) => {
|
|
1975
|
+
const normalizedValue = normalizeFilterLiteral(v);
|
|
1976
|
+
return (normalizedValue.length >= MIN_FUZZY_LITERAL_LENGTH &&
|
|
1977
|
+
(normalizedValue.startsWith(normalizedLiteral) ||
|
|
1978
|
+
normalizedLiteral.startsWith(normalizedValue)));
|
|
1979
|
+
})));
|
|
1980
|
+
return prefixMatches.length === 1 ? prefixMatches[0] : undefined;
|
|
1981
|
+
}
|
|
1982
|
+
// Rewrites `"col" = 'literal'` predicates in the WHERE clause whose literal
|
|
1983
|
+
// fuzzy-matches a real probed column value, returning the updated SQL and
|
|
1984
|
+
// whether anything changed. Every distinct predicate is corrected (a column can
|
|
1985
|
+
// appear with several literals across `OR` branches), and only the WHERE clause
|
|
1986
|
+
// is scanned so JOIN ON / HAVING literals are left untouched.
|
|
1987
|
+
async function correctEqualityFilters(sql, probeColumnValues, setMessages) {
|
|
1988
|
+
const whereClause = extractWhereClause(sql);
|
|
1989
|
+
if (whereClause === undefined) {
|
|
1990
|
+
return { sql, corrected: false };
|
|
1991
|
+
}
|
|
1992
|
+
const literalsByColumn = new Map();
|
|
1993
|
+
for (const match of whereClause.matchAll(EQUALITY_PREDICATE_PATTERN)) {
|
|
1994
|
+
const column = match.groups?.col;
|
|
1995
|
+
const literal = match.groups?.val;
|
|
1996
|
+
if (column === undefined || literal === undefined) {
|
|
1997
|
+
continue;
|
|
1998
|
+
}
|
|
1999
|
+
const literals = literalsByColumn.get(column) ?? new Set();
|
|
2000
|
+
literals.add(literal);
|
|
2001
|
+
literalsByColumn.set(column, literals);
|
|
2002
|
+
}
|
|
2003
|
+
const resolutions = new Map();
|
|
2004
|
+
for (const [column, literals] of literalsByColumn) {
|
|
2005
|
+
const values = await probeColumnValues(column);
|
|
2006
|
+
if (values === undefined) {
|
|
2007
|
+
continue;
|
|
2008
|
+
}
|
|
2009
|
+
for (const literal of literals) {
|
|
2010
|
+
const resolved = resolveFilterLiteral(literal, values);
|
|
2011
|
+
if (resolved !== undefined && resolved !== literal) {
|
|
2012
|
+
const columnResolutions = resolutions.get(column) ?? new Map();
|
|
2013
|
+
columnResolutions.set(literal, resolved);
|
|
2014
|
+
resolutions.set(column, columnResolutions);
|
|
2015
|
+
addThinkingStep(setMessages, `Matched "${column}" = '${literal}' to '${resolved}' from the column's values...`);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
if (resolutions.size === 0) {
|
|
2020
|
+
return { sql, corrected: false };
|
|
2021
|
+
}
|
|
2022
|
+
const correctedWhere = whereClause.replace(EQUALITY_PREDICATE_PATTERN, (matched, column, literal) => {
|
|
2023
|
+
const resolved = resolutions.get(column)?.get(literal);
|
|
2024
|
+
return resolved === undefined
|
|
2025
|
+
? matched
|
|
2026
|
+
: matched.replace(`'${literal}'`, () => `'${escapeSqlStringLiteral(resolved)}'`);
|
|
2027
|
+
});
|
|
2028
|
+
return {
|
|
2029
|
+
sql: sql.replace(whereClause, () => correctedWhere),
|
|
2030
|
+
corrected: true,
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
// Rewrites the literals of a single `IN (...)` list to their fuzzy-matched
|
|
2034
|
+
// column values; returns the joined list text and whether anything changed.
|
|
2035
|
+
function rewriteInListLiterals(list, column, values, setMessages) {
|
|
2036
|
+
let changed = false;
|
|
2037
|
+
const rewrittenList = [];
|
|
2038
|
+
for (const literalMatch of list.matchAll(IN_LIST_LITERAL_PATTERN)) {
|
|
2039
|
+
const literal = literalMatch.groups?.val;
|
|
2040
|
+
if (literal === undefined) {
|
|
2041
|
+
continue;
|
|
2042
|
+
}
|
|
2043
|
+
const resolved = resolveFilterLiteral(literal, values);
|
|
2044
|
+
if (resolved !== undefined && resolved !== literal) {
|
|
2045
|
+
rewrittenList.push(`'${escapeSqlStringLiteral(resolved)}'`);
|
|
2046
|
+
changed = true;
|
|
2047
|
+
addThinkingStep(setMessages, `Matched "${column}" IN '${literal}' to '${resolved}' from the column's values...`);
|
|
2048
|
+
}
|
|
2049
|
+
else {
|
|
2050
|
+
rewrittenList.push(`'${literal}'`);
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
return { rewritten: rewrittenList.join(', '), changed };
|
|
2054
|
+
}
|
|
2055
|
+
async function correctInPredicateFilters(sql, probeColumnValues, setMessages) {
|
|
2056
|
+
const whereClause = extractWhereClause(sql);
|
|
2057
|
+
if (whereClause === undefined) {
|
|
2058
|
+
return { sql, corrected: false };
|
|
2059
|
+
}
|
|
2060
|
+
const columns = new Set();
|
|
2061
|
+
for (const match of whereClause.matchAll(IN_PREDICATE_PATTERN)) {
|
|
2062
|
+
const column = match.groups?.col;
|
|
2063
|
+
if (column !== undefined) {
|
|
2064
|
+
columns.add(column);
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
const valuesByColumn = new Map();
|
|
2068
|
+
for (const column of columns) {
|
|
2069
|
+
const values = await probeColumnValues(column);
|
|
2070
|
+
if (values !== undefined) {
|
|
2071
|
+
valuesByColumn.set(column, values);
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
if (valuesByColumn.size === 0) {
|
|
2075
|
+
return { sql, corrected: false };
|
|
2076
|
+
}
|
|
2077
|
+
const correctedWhere = whereClause.replace(IN_PREDICATE_PATTERN, (matched, column, list) => {
|
|
2078
|
+
const values = valuesByColumn.get(column);
|
|
2079
|
+
if (values === undefined) {
|
|
2080
|
+
return matched;
|
|
2081
|
+
}
|
|
2082
|
+
const { rewritten, changed } = rewriteInListLiterals(list, column, values, setMessages);
|
|
2083
|
+
return changed && rewritten.length > 0
|
|
2084
|
+
? matched.replace(list, () => rewritten)
|
|
2085
|
+
: matched;
|
|
2086
|
+
});
|
|
2087
|
+
return correctedWhere === whereClause
|
|
2088
|
+
? { sql, corrected: false }
|
|
2089
|
+
: { sql: sql.replace(whereClause, () => correctedWhere), corrected: true };
|
|
2090
|
+
}
|
|
2091
|
+
// Distinct columns filtered by equality or IN predicates in the WHERE clause,
|
|
2092
|
+
// in encounter order — the set the zero-row recovery probes for real values.
|
|
2093
|
+
function collectWhereFilterColumns(sql) {
|
|
2094
|
+
const whereClause = extractWhereClause(sql);
|
|
2095
|
+
if (whereClause === undefined) {
|
|
2096
|
+
return [];
|
|
2097
|
+
}
|
|
2098
|
+
const columns = [];
|
|
2099
|
+
const seen = new Set();
|
|
2100
|
+
const addColumn = (column) => {
|
|
2101
|
+
if (column !== undefined && !seen.has(column)) {
|
|
2102
|
+
seen.add(column);
|
|
2103
|
+
columns.push(column);
|
|
2104
|
+
}
|
|
2105
|
+
};
|
|
2106
|
+
for (const match of whereClause.matchAll(EQUALITY_PREDICATE_PATTERN)) {
|
|
2107
|
+
addColumn(match.groups?.col);
|
|
2108
|
+
}
|
|
2109
|
+
for (const match of whereClause.matchAll(IN_PREDICATE_PATTERN)) {
|
|
2110
|
+
addColumn(match.groups?.col);
|
|
2111
|
+
}
|
|
2112
|
+
return columns;
|
|
2113
|
+
}
|
|
2114
|
+
// Indexes each access point by its lowercased column names (first writer wins),
|
|
2115
|
+
// so a filter column resolves to the access point that declares it.
|
|
2116
|
+
function buildAccessPointColumnIndex(services) {
|
|
2117
|
+
const index = new Map();
|
|
2118
|
+
for (const accessPoint of services) {
|
|
2119
|
+
if (accessPoint.sourceType !== TDSServiceSourceType.ACCESS_POINT) {
|
|
2120
|
+
continue;
|
|
2121
|
+
}
|
|
2122
|
+
for (const column of accessPoint.columns) {
|
|
2123
|
+
const key = column.name.toLowerCase();
|
|
2124
|
+
if (!index.has(key)) {
|
|
2125
|
+
index.set(key, accessPoint);
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
return index;
|
|
2130
|
+
}
|
|
2131
|
+
// Recovers a zero-row query by re-probing each WHERE-filter column's real distinct
|
|
2132
|
+
// values and rewriting equality/IN literals that don't match (casing/spelling).
|
|
2133
|
+
async function attemptDistinctValueRecovery(currentSql, selectedServices, dataProductCoordinates, context) {
|
|
2134
|
+
const { plugin, config, setMessages } = context;
|
|
2135
|
+
const isAccessPointProbe = dataProductCoordinates !== undefined &&
|
|
2136
|
+
selectedServices.some((s) => s.sourceType === TDSServiceSourceType.ACCESS_POINT);
|
|
2137
|
+
const accessPointByColumn = isAccessPointProbe
|
|
2138
|
+
? buildAccessPointColumnIndex(selectedServices)
|
|
2139
|
+
: new Map();
|
|
2140
|
+
const fromClause = isAccessPointProbe
|
|
2141
|
+
? undefined
|
|
2142
|
+
: extractFromClause(currentSql);
|
|
2143
|
+
if (!isAccessPointProbe && fromClause === undefined) {
|
|
2144
|
+
return undefined;
|
|
2145
|
+
}
|
|
2146
|
+
const probeAccessPointColumn = async (column) => {
|
|
2147
|
+
const accessPoint = accessPointByColumn.get(column.toLowerCase());
|
|
2148
|
+
const pId = accessPoint ? servicePId(accessPoint) : undefined;
|
|
2149
|
+
if (pId === undefined) {
|
|
2150
|
+
return { columns: [], rows: [] };
|
|
2151
|
+
}
|
|
2152
|
+
const relationQuery = `#P{${pId}}#->select(~[${pureRelationColumnRef(column)}])->distinct()->take(${DISTINCT_PROBE_ROW_LIMIT})`;
|
|
2153
|
+
return plugin.executeLakehouseRelationQuery(relationQuery, guaranteeNonNullable(dataProductCoordinates), config);
|
|
2154
|
+
};
|
|
2155
|
+
const probedValues = new Map();
|
|
2156
|
+
const probeColumnValues = async (column) => {
|
|
2157
|
+
if (probedValues.has(column)) {
|
|
2158
|
+
return probedValues.get(column);
|
|
2159
|
+
}
|
|
2160
|
+
if (probedValues.size >= MAX_RECOVERED_FILTER_COLUMNS) {
|
|
2161
|
+
return undefined;
|
|
2162
|
+
}
|
|
2163
|
+
let values;
|
|
2164
|
+
try {
|
|
2165
|
+
const probe = isAccessPointProbe
|
|
2166
|
+
? await probeAccessPointColumn(column)
|
|
2167
|
+
: await executeSqlForServices(`SELECT DISTINCT "${column}" ${fromClause} LIMIT ${DISTINCT_PROBE_ROW_LIMIT}`, selectedServices, dataProductCoordinates, plugin, config);
|
|
2168
|
+
values = probe.rows
|
|
2169
|
+
.map((row) => row[column])
|
|
2170
|
+
.filter(isNonNullable)
|
|
2171
|
+
.map(String);
|
|
2172
|
+
}
|
|
2173
|
+
catch (probeError) {
|
|
2174
|
+
assertErrorThrown(probeError);
|
|
2175
|
+
addThinkingStep(setMessages, `Could not probe values for "${column}"`);
|
|
2176
|
+
values = undefined;
|
|
2177
|
+
}
|
|
2178
|
+
probedValues.set(column, values);
|
|
2179
|
+
return values;
|
|
2180
|
+
};
|
|
2181
|
+
await Promise.all(collectWhereFilterColumns(currentSql)
|
|
2182
|
+
.slice(0, MAX_RECOVERED_FILTER_COLUMNS)
|
|
2183
|
+
.map((column) => probeColumnValues(column)));
|
|
2184
|
+
const equalityPass = await correctEqualityFilters(currentSql, probeColumnValues, setMessages);
|
|
2185
|
+
const inPass = await correctInPredicateFilters(equalityPass.sql, probeColumnValues, setMessages);
|
|
2186
|
+
const correctedSql = inPass.sql;
|
|
2187
|
+
const corrected = equalityPass.corrected || inPass.corrected;
|
|
2188
|
+
if (!corrected) {
|
|
2189
|
+
return undefined;
|
|
2190
|
+
}
|
|
2191
|
+
try {
|
|
2192
|
+
const retryResult = await executeSqlForServices(correctedSql, selectedServices, dataProductCoordinates, plugin, config);
|
|
2193
|
+
if (retryResult.rows.length > 0) {
|
|
2194
|
+
return { sql: correctedSql, result: retryResult };
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
catch (retryError) {
|
|
2198
|
+
assertErrorThrown(retryError);
|
|
2199
|
+
addThinkingStep(setMessages, 'Filter-recovery retry failed');
|
|
2200
|
+
}
|
|
2201
|
+
return undefined;
|
|
2202
|
+
}
|
|
1713
2203
|
async function tryRecoverZeroRows(currentSql, sqlResult, question, selectedServices, coordinates, dataProductCoordinates, context) {
|
|
1714
2204
|
const { plugin, config, setMessages } = context;
|
|
1715
2205
|
let recoveredSql = currentSql;
|
|
1716
2206
|
let recoveredResult = sqlResult;
|
|
2207
|
+
let selfHealed = LegendAISelfHealKind.NONE;
|
|
1717
2208
|
const strippedSql = stripGuessedNonDateServiceParams(recoveredSql, question);
|
|
1718
2209
|
if (strippedSql !== recoveredSql) {
|
|
1719
2210
|
addThinkingStep(setMessages, 'Trying query without guessed parameter values...');
|
|
@@ -1730,14 +2221,25 @@ async function tryRecoverZeroRows(currentSql, sqlResult, question, selectedServi
|
|
|
1730
2221
|
addThinkingStep(setMessages, `Parameter stripping recovery failed: ${stripError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
1731
2222
|
}
|
|
1732
2223
|
}
|
|
2224
|
+
if (recoveredResult.rows.length === 0) {
|
|
2225
|
+
addThinkingStep(setMessages, "Checking filter values against the column's actual values...");
|
|
2226
|
+
const resolved = await attemptDistinctValueRecovery(recoveredSql, selectedServices, dataProductCoordinates, context);
|
|
2227
|
+
if (resolved) {
|
|
2228
|
+
recoveredSql = resolved.sql;
|
|
2229
|
+
recoveredResult = resolved.result;
|
|
2230
|
+
selfHealed = LegendAISelfHealKind.DISTINCT_VALUE;
|
|
2231
|
+
updateLastAssistant(setMessages, () => ({ sql: resolved.sql }));
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
1733
2234
|
if (recoveredResult.rows.length === 0) {
|
|
1734
2235
|
const correction = await attemptZeroRowCorrection(recoveredSql, question, selectedServices, coordinates, dataProductCoordinates, context);
|
|
1735
2236
|
if (correction) {
|
|
1736
2237
|
recoveredSql = correction.sql;
|
|
1737
2238
|
recoveredResult = correction.result;
|
|
2239
|
+
selfHealed = LegendAISelfHealKind.LLM_ZERO_ROW;
|
|
1738
2240
|
}
|
|
1739
2241
|
}
|
|
1740
|
-
return { sql: recoveredSql, result: recoveredResult };
|
|
2242
|
+
return { sql: recoveredSql, result: recoveredResult, selfHealed };
|
|
1741
2243
|
}
|
|
1742
2244
|
async function resolveNestedPCalls(sql, question, selectedAPs, context, modelContextEnrichment) {
|
|
1743
2245
|
if (!hasNestedPCalls(sql)) {
|
|
@@ -1797,11 +2299,64 @@ async function retryAccessPointSqlFix(options, context, metadata) {
|
|
|
1797
2299
|
* - Uses AP-specific generator/judge prompts (p() syntax, no coordinates/params)
|
|
1798
2300
|
* - Skips parameter detection (APs have no parameters)
|
|
1799
2301
|
*/
|
|
2302
|
+
// Handles a failed access-point SQL execution: attempts an SQL-fix retry for
|
|
2303
|
+
// fixable errors, otherwise reports the execution error with fallback suggestions.
|
|
2304
|
+
async function handleAccessPointExecutionError(executeError, params, context) {
|
|
2305
|
+
const { setMessages } = context;
|
|
2306
|
+
const { finalSql, question, selectedAPs, dataProductCoordinates, modelContextEnrichment, startTime, execStartTime, metadata, } = params;
|
|
2307
|
+
const execErrorType = classifyError(executeError);
|
|
2308
|
+
const errMsg = executeError.message;
|
|
2309
|
+
const errorCategory = categorizeExecutionError(errMsg, executeError);
|
|
2310
|
+
addThinkingStep(setMessages, `Execution failed: ${errMsg.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
2311
|
+
if (errorCategory === ExecutionErrorCategory.SQL_FIXABLE) {
|
|
2312
|
+
const retried = await retryAccessPointSqlFix({
|
|
2313
|
+
failedSql: finalSql,
|
|
2314
|
+
question,
|
|
2315
|
+
errMsg,
|
|
2316
|
+
selectedAPs,
|
|
2317
|
+
dataProductCoordinates,
|
|
2318
|
+
...(modelContextEnrichment === undefined
|
|
2319
|
+
? {}
|
|
2320
|
+
: { modelContextEnrichment }),
|
|
2321
|
+
startTime,
|
|
2322
|
+
}, context, metadata);
|
|
2323
|
+
if (retried) {
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
finishWithThinkingError(setMessages, buildExecutionErrorMessage(errMsg, selectedAPs), startTime, execErrorType === LegendAIErrorType.GENERAL
|
|
2328
|
+
? LegendAIErrorType.EXECUTION
|
|
2329
|
+
: execErrorType);
|
|
2330
|
+
const queriedGroups = [
|
|
2331
|
+
...new Set(selectedAPs
|
|
2332
|
+
.map((ap) => ap.accessPointGroupTitle)
|
|
2333
|
+
.filter((t) => t !== undefined)),
|
|
2334
|
+
];
|
|
2335
|
+
updateLastAssistant(setMessages, () => ({
|
|
2336
|
+
execTime: elapsedSeconds(execStartTime, 2),
|
|
2337
|
+
isExecuting: false,
|
|
2338
|
+
suggestedQueries: buildFallbackSuggestions(selectedAPs),
|
|
2339
|
+
queriedAccessPointGroups: queriedGroups,
|
|
2340
|
+
}));
|
|
2341
|
+
}
|
|
1800
2342
|
async function processAccessPointQuery(question, accessPoints, metadata, context, startTime, dataProductCoordinates, modelContextEnrichment) {
|
|
1801
2343
|
const { config, plugin, setMessages } = context;
|
|
1802
2344
|
addThinkingStep(setMessages, 'Found relevant access points to query');
|
|
1803
2345
|
const selectedAPs = await selectBestServices(question, accessPoints, context);
|
|
1804
|
-
const
|
|
2346
|
+
const currentAccessPoints = new Set(selectedAPs.map((ap) => ap.pattern.replace(/^\//, '')));
|
|
2347
|
+
const scopedContext = {
|
|
2348
|
+
...context,
|
|
2349
|
+
history: filterHistoryForAccessPoints(context.history, currentAccessPoints),
|
|
2350
|
+
};
|
|
2351
|
+
try {
|
|
2352
|
+
addThinkingStep(setMessages, 'Grounding filter values from real data...');
|
|
2353
|
+
await withTimeout(plugin.enrichAccessPointSampleValues(selectedAPs, dataProductCoordinates, config), VALUE_GROUNDING_TIME_BUDGET_MS);
|
|
2354
|
+
}
|
|
2355
|
+
catch (error) {
|
|
2356
|
+
assertErrorThrown(error);
|
|
2357
|
+
addThinkingStep(setMessages, 'Value grounding skipped');
|
|
2358
|
+
}
|
|
2359
|
+
const judgedSql = await generateAndJudgeAccessPointSql(question, selectedAPs, scopedContext, startTime, modelContextEnrichment);
|
|
1805
2360
|
if (!judgedSql) {
|
|
1806
2361
|
addThinkingStep(setMessages, 'SQL generation could not produce a valid query.');
|
|
1807
2362
|
handleSqlGenerationFailure(setMessages, startTime, false, SQL_GENERATION_FAILURE_WITH_ORCHESTRATOR, SQL_GENERATION_FAILURE_NO_ORCHESTRATOR, LegendAIErrorType.GENERATION, buildFallbackSuggestions(selectedAPs));
|
|
@@ -1809,61 +2364,51 @@ async function processAccessPointQuery(question, accessPoints, metadata, context
|
|
|
1809
2364
|
}
|
|
1810
2365
|
const sqlGenTimeValue = elapsedSeconds(startTime, 2);
|
|
1811
2366
|
completeThinkingSteps(setMessages);
|
|
1812
|
-
const finalSql = await resolveNestedPCalls(judgedSql, question, selectedAPs,
|
|
2367
|
+
const finalSql = await resolveNestedPCalls(judgedSql, question, selectedAPs, scopedContext, modelContextEnrichment);
|
|
1813
2368
|
updateLastAssistant(setMessages, () => ({
|
|
1814
2369
|
sql: finalSql,
|
|
1815
2370
|
sqlGenTime: sqlGenTimeValue,
|
|
1816
2371
|
isExecuting: true,
|
|
2372
|
+
queriedAccessPoints: orderedAccessPointsFromSql(finalSql),
|
|
1817
2373
|
}));
|
|
1818
2374
|
const execStartTime = Date.now();
|
|
1819
2375
|
try {
|
|
1820
|
-
|
|
2376
|
+
let execSql = finalSql;
|
|
2377
|
+
let rawResult = await executeSqlForServices(execSql, selectedAPs, dataProductCoordinates, plugin, config);
|
|
2378
|
+
if (rawResult.rows.length === 0) {
|
|
2379
|
+
addThinkingStep(setMessages, "Checking filter values against the column's actual values...");
|
|
2380
|
+
const resolved = await attemptDistinctValueRecovery(execSql, selectedAPs, dataProductCoordinates, scopedContext);
|
|
2381
|
+
if (resolved) {
|
|
2382
|
+
execSql = resolved.sql;
|
|
2383
|
+
rawResult = resolved.result;
|
|
2384
|
+
updateLastAssistant(setMessages, () => ({ sql: execSql }));
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
1821
2387
|
await reportQueryResults({
|
|
1822
|
-
currentSql:
|
|
2388
|
+
currentSql: execSql,
|
|
1823
2389
|
sqlResult: rawResult,
|
|
1824
2390
|
question,
|
|
1825
2391
|
services: selectedAPs,
|
|
1826
|
-
|
|
2392
|
+
allAccessPoints: accessPoints,
|
|
2393
|
+
}, metadata, scopedContext, startTime, false);
|
|
1827
2394
|
}
|
|
1828
2395
|
catch (executeError) {
|
|
1829
2396
|
assertErrorThrown(executeError);
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
}, context, metadata);
|
|
1846
|
-
if (retried) {
|
|
1847
|
-
return;
|
|
1848
|
-
}
|
|
1849
|
-
}
|
|
1850
|
-
finishWithThinkingError(setMessages, buildExecutionErrorMessage(errMsg, selectedAPs), startTime, execErrorType === LegendAIErrorType.GENERAL
|
|
1851
|
-
? LegendAIErrorType.EXECUTION
|
|
1852
|
-
: execErrorType);
|
|
1853
|
-
const queriedGroups = [
|
|
1854
|
-
...new Set(selectedAPs
|
|
1855
|
-
.map((ap) => ap.accessPointGroupTitle)
|
|
1856
|
-
.filter((t) => t !== undefined)),
|
|
1857
|
-
];
|
|
1858
|
-
updateLastAssistant(setMessages, () => ({
|
|
1859
|
-
execTime: elapsedSeconds(execStartTime, 2),
|
|
1860
|
-
isExecuting: false,
|
|
1861
|
-
suggestedQueries: buildFallbackSuggestions(selectedAPs),
|
|
1862
|
-
queriedAccessPointGroups: queriedGroups,
|
|
1863
|
-
}));
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
|
-
async function processDataQuery(question, services, coordinates, metadata, context, startTime, orchestratorOptions, modelContextEnrichment) {
|
|
2397
|
+
await handleAccessPointExecutionError(executeError, {
|
|
2398
|
+
finalSql,
|
|
2399
|
+
question,
|
|
2400
|
+
selectedAPs,
|
|
2401
|
+
dataProductCoordinates,
|
|
2402
|
+
...(modelContextEnrichment === undefined
|
|
2403
|
+
? {}
|
|
2404
|
+
: { modelContextEnrichment }),
|
|
2405
|
+
startTime,
|
|
2406
|
+
execStartTime,
|
|
2407
|
+
metadata,
|
|
2408
|
+
}, scopedContext);
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
async function processDataQuery(question, services, coordinates, metadata, context, startTime, orchestratorOptions, modelContextEnrichment, approachText) {
|
|
1867
2412
|
const { config, setMessages } = context;
|
|
1868
2413
|
const dataProductCoordinates = orchestratorOptions?.dataProductCoordinates;
|
|
1869
2414
|
const hasOrchestratorFallback = Boolean(config.orchestratorUrl && dataProductCoordinates);
|
|
@@ -1940,6 +2485,9 @@ async function processDataQuery(question, services, coordinates, metadata, conte
|
|
|
1940
2485
|
question,
|
|
1941
2486
|
services: selectedServices,
|
|
1942
2487
|
}, metadata, context, startTime, hasOrchestratorFallback);
|
|
2488
|
+
if (approachText) {
|
|
2489
|
+
attachMetadataOverview(setMessages, approachText);
|
|
2490
|
+
}
|
|
1943
2491
|
}
|
|
1944
2492
|
function splitServicesByType(services) {
|
|
1945
2493
|
return {
|
|
@@ -1947,7 +2495,7 @@ function splitServicesByType(services) {
|
|
|
1947
2495
|
accessPoints: services.filter((s) => s.sourceType === TDSServiceSourceType.ACCESS_POINT),
|
|
1948
2496
|
};
|
|
1949
2497
|
}
|
|
1950
|
-
async function routeToAccessPointOrData(question, services, coordinates, metadata, context, startTime, orchestratorOpts, modelContextEnrichment) {
|
|
2498
|
+
async function routeToAccessPointOrData(question, services, coordinates, metadata, context, startTime, orchestratorOpts, modelContextEnrichment, approachText) {
|
|
1951
2499
|
const dataProductCoordinates = orchestratorOpts?.dataProductCoordinates;
|
|
1952
2500
|
const { tdsServices, accessPoints } = splitServicesByType(services);
|
|
1953
2501
|
if (tdsServices.length === 0 &&
|
|
@@ -1956,15 +2504,37 @@ async function routeToAccessPointOrData(question, services, coordinates, metadat
|
|
|
1956
2504
|
await processAccessPointQuery(question, accessPoints, metadata, context, startTime, dataProductCoordinates, modelContextEnrichment);
|
|
1957
2505
|
}
|
|
1958
2506
|
else {
|
|
1959
|
-
await processDataQuery(question, tdsServices, coordinates, metadata, context, startTime, orchestratorOpts, modelContextEnrichment);
|
|
2507
|
+
await processDataQuery(question, tdsServices, coordinates, metadata, context, startTime, orchestratorOpts, modelContextEnrichment, approachText);
|
|
1960
2508
|
}
|
|
1961
2509
|
}
|
|
2510
|
+
/**
|
|
2511
|
+
* Compute a value on first use and cache it. The data-query and metadata
|
|
2512
|
+
* answer paths each need a different (potentially large) model-context string,
|
|
2513
|
+
* but only one path runs per question. Deferring the build means we never pay
|
|
2514
|
+
* to construct the string the chosen path doesn't consume, and the build
|
|
2515
|
+
* happens inside the caller's try/catch rather than eagerly at entry.
|
|
2516
|
+
*/
|
|
2517
|
+
function lazyMemo(compute) {
|
|
2518
|
+
let computed = false;
|
|
2519
|
+
let value;
|
|
2520
|
+
return () => {
|
|
2521
|
+
if (!computed) {
|
|
2522
|
+
value = compute();
|
|
2523
|
+
computed = true;
|
|
2524
|
+
}
|
|
2525
|
+
return value;
|
|
2526
|
+
};
|
|
2527
|
+
}
|
|
1962
2528
|
export async function processQuestion(question, services, coordinates, metadata, context, dataProductCoordinates, pureExecutionContext, modelContext) {
|
|
1963
2529
|
const { config, plugin, setMessages } = context;
|
|
1964
2530
|
const startTime = Date.now();
|
|
1965
|
-
const
|
|
2531
|
+
const getModelContextEnrichment = lazyMemo(() => modelContext
|
|
1966
2532
|
? buildModelContextEnrichmentText(modelContext, services)
|
|
1967
|
-
: undefined;
|
|
2533
|
+
: undefined);
|
|
2534
|
+
const getMetadataEnrichment = lazyMemo(() => modelContext ? buildModelCatalogText(modelContext) : undefined);
|
|
2535
|
+
const getApproachText = lazyMemo(() => modelContext
|
|
2536
|
+
? buildDataQueryApproachText(question, modelContext)
|
|
2537
|
+
: undefined);
|
|
1968
2538
|
try {
|
|
1969
2539
|
addThinkingStep(setMessages, 'Analyzing your question...');
|
|
1970
2540
|
const orchestratorOpts = dataProductCoordinates
|
|
@@ -1976,7 +2546,7 @@ export async function processQuestion(question, services, coordinates, metadata,
|
|
|
1976
2546
|
}
|
|
1977
2547
|
: undefined;
|
|
1978
2548
|
if (services.length === 0) {
|
|
1979
|
-
await handleMetadataQuestion(question, metadata, context, startTime, false, services,
|
|
2549
|
+
await handleMetadataQuestion(question, metadata, context, startTime, false, services, getMetadataEnrichment());
|
|
1980
2550
|
if (config.orchestratorUrl && dataProductCoordinates) {
|
|
1981
2551
|
updateLastAssistant(setMessages, () => ({
|
|
1982
2552
|
fallbackAction: {
|
|
@@ -1990,16 +2560,16 @@ export async function processQuestion(question, services, coordinates, metadata,
|
|
|
1990
2560
|
const serviceNames = services.map((s) => s.title);
|
|
1991
2561
|
const intent = await plugin.classifyQuestionIntent(question, true, config, serviceNames);
|
|
1992
2562
|
if (intent === LegendAIQuestionIntent.METADATA) {
|
|
1993
|
-
await handleMetadataQuestion(question, metadata, context, startTime, true, services,
|
|
2563
|
+
await handleMetadataQuestion(question, metadata, context, startTime, true, services, getMetadataEnrichment());
|
|
1994
2564
|
return;
|
|
1995
2565
|
}
|
|
1996
2566
|
try {
|
|
1997
|
-
await routeToAccessPointOrData(question, services, coordinates, metadata, context, startTime, orchestratorOpts,
|
|
2567
|
+
await routeToAccessPointOrData(question, services, coordinates, metadata, context, startTime, orchestratorOpts, getModelContextEnrichment(), getApproachText());
|
|
1998
2568
|
}
|
|
1999
2569
|
catch (sqlError) {
|
|
2000
2570
|
assertErrorThrown(sqlError);
|
|
2001
2571
|
addThinkingStep(setMessages, 'Query failed, answering from product metadata...');
|
|
2002
|
-
await handleMetadataQuestion(question, metadata, context, startTime, true, services,
|
|
2572
|
+
await handleMetadataQuestion(question, metadata, context, startTime, true, services, getMetadataEnrichment());
|
|
2003
2573
|
appendFallbackSuggestions(setMessages, services);
|
|
2004
2574
|
}
|
|
2005
2575
|
}
|
|
@@ -2010,12 +2580,12 @@ export async function processQuestion(question, services, coordinates, metadata,
|
|
|
2010
2580
|
appendFallbackSuggestions(setMessages, services);
|
|
2011
2581
|
}
|
|
2012
2582
|
}
|
|
2013
|
-
async function routeDataQueryWithErrorHandling(question, services, coordinates, metadata, context, orchestratorOptions, modelContextEnrichment) {
|
|
2583
|
+
async function routeDataQueryWithErrorHandling(question, services, coordinates, metadata, context, orchestratorOptions, modelContextEnrichment, approachText) {
|
|
2014
2584
|
const { setMessages } = context;
|
|
2015
2585
|
const startTime = Date.now();
|
|
2016
2586
|
try {
|
|
2017
2587
|
addThinkingStep(setMessages, 'Preparing data query...');
|
|
2018
|
-
await routeToAccessPointOrData(question, services, coordinates, metadata, context, startTime, orchestratorOptions, modelContextEnrichment);
|
|
2588
|
+
await routeToAccessPointOrData(question, services, coordinates, metadata, context, startTime, orchestratorOptions, modelContextEnrichment, approachText);
|
|
2019
2589
|
}
|
|
2020
2590
|
catch (error) {
|
|
2021
2591
|
assertErrorThrown(error);
|
|
@@ -2026,13 +2596,17 @@ async function routeDataQueryWithErrorHandling(question, services, coordinates,
|
|
|
2026
2596
|
export async function processQuestionWithIntent(question, intent, services, coordinates, metadata, context, orchestratorOptions, modelContext) {
|
|
2027
2597
|
const { config, setMessages } = context;
|
|
2028
2598
|
const dataProductCoordinates = orchestratorOptions?.dataProductCoordinates;
|
|
2029
|
-
const
|
|
2599
|
+
const getModelContextEnrichment = lazyMemo(() => modelContext
|
|
2030
2600
|
? buildModelContextEnrichmentText(modelContext, services)
|
|
2031
|
-
: undefined;
|
|
2601
|
+
: undefined);
|
|
2602
|
+
const getMetadataEnrichment = lazyMemo(() => modelContext ? buildModelCatalogText(modelContext) : undefined);
|
|
2603
|
+
const getApproachText = lazyMemo(() => modelContext
|
|
2604
|
+
? buildDataQueryApproachText(question, modelContext)
|
|
2605
|
+
: undefined);
|
|
2032
2606
|
if (intent === LegendAIQuestionIntent.METADATA) {
|
|
2033
2607
|
const startTime = Date.now();
|
|
2034
2608
|
try {
|
|
2035
|
-
await handleMetadataQuestion(question, metadata, context, startTime, services.length > 0, services,
|
|
2609
|
+
await handleMetadataQuestion(question, metadata, context, startTime, services.length > 0, services, getMetadataEnrichment());
|
|
2036
2610
|
}
|
|
2037
2611
|
catch (error) {
|
|
2038
2612
|
assertErrorThrown(error);
|
|
@@ -2045,11 +2619,11 @@ export async function processQuestionWithIntent(question, intent, services, coor
|
|
|
2045
2619
|
config.orchestratorUrl &&
|
|
2046
2620
|
dataProductCoordinates) {
|
|
2047
2621
|
if (services.length > 0) {
|
|
2048
|
-
await routeDataQueryWithErrorHandling(question, services, coordinates, metadata, context, orchestratorOptions,
|
|
2622
|
+
await routeDataQueryWithErrorHandling(question, services, coordinates, metadata, context, orchestratorOptions, getModelContextEnrichment(), getApproachText());
|
|
2049
2623
|
return;
|
|
2050
2624
|
}
|
|
2051
2625
|
const startTime = Date.now();
|
|
2052
|
-
await handleMetadataQuestion(question, metadata, context, startTime, false, services,
|
|
2626
|
+
await handleMetadataQuestion(question, metadata, context, startTime, false, services, getMetadataEnrichment());
|
|
2053
2627
|
updateLastAssistant(setMessages, () => ({
|
|
2054
2628
|
fallbackAction: {
|
|
2055
2629
|
label: ORCHESTRATOR_FALLBACK_LABEL,
|
|
@@ -2058,6 +2632,6 @@ export async function processQuestionWithIntent(question, intent, services, coor
|
|
|
2058
2632
|
}));
|
|
2059
2633
|
return;
|
|
2060
2634
|
}
|
|
2061
|
-
await routeDataQueryWithErrorHandling(question, services, coordinates, metadata, context, orchestratorOptions,
|
|
2635
|
+
await routeDataQueryWithErrorHandling(question, services, coordinates, metadata, context, orchestratorOptions, getModelContextEnrichment(), getApproachText());
|
|
2062
2636
|
}
|
|
2063
2637
|
//# sourceMappingURL=LegendAIChatProcessors.js.map
|