@finos/legend-lego 2.0.202 → 2.0.204
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.css +2 -2
- package/lib/index.css.map +1 -1
- package/lib/legend-ai/LegendAIDocEnrichment.d.ts +34 -1
- package/lib/legend-ai/LegendAIDocEnrichment.d.ts.map +1 -1
- package/lib/legend-ai/LegendAIDocEnrichment.js +890 -1
- package/lib/legend-ai/LegendAIDocEnrichment.js.map +1 -1
- package/lib/legend-ai/LegendAIServiceRetrieval.d.ts +36 -0
- package/lib/legend-ai/LegendAIServiceRetrieval.d.ts.map +1 -0
- package/lib/legend-ai/LegendAIServiceRetrieval.js +179 -0
- package/lib/legend-ai/LegendAIServiceRetrieval.js.map +1 -0
- package/lib/legend-ai/LegendAITypes.d.ts +62 -2
- package/lib/legend-ai/LegendAITypes.d.ts.map +1 -1
- package/lib/legend-ai/LegendAITypes.js +56 -30
- package/lib/legend-ai/LegendAITypes.js.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts +29 -6
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js +9 -0
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js.map +1 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.d.ts +5 -0
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.d.ts.map +1 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.js +18 -2
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.js.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisPanel.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisPanel.js +27 -8
- package/lib/legend-ai/components/LegendAIAnalysisPanel.js.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts +1 -0
- package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisUtils.js +144 -22
- package/lib/legend-ai/components/LegendAIAnalysisUtils.js.map +1 -1
- package/lib/legend-ai/components/LegendAICharts.d.ts +4 -0
- package/lib/legend-ai/components/LegendAICharts.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAICharts.js +60 -1
- package/lib/legend-ai/components/LegendAICharts.js.map +1 -1
- package/lib/legend-ai/components/LegendAIChat.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIChat.js +46 -17
- package/lib/legend-ai/components/LegendAIChat.js.map +1 -1
- package/lib/legend-ai/components/LegendAIChatHelpers.d.ts +1 -1
- package/lib/legend-ai/components/LegendAIChatHelpers.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIChatHelpers.js +220 -15
- package/lib/legend-ai/components/LegendAIChatHelpers.js.map +1 -1
- package/lib/legend-ai/components/LegendAIChatInput.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIChatInput.js +1 -0
- package/lib/legend-ai/components/LegendAIChatInput.js.map +1 -1
- package/lib/legend-ai/components/LegendAIScopeSelector.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIScopeSelector.js +5 -2
- package/lib/legend-ai/components/LegendAIScopeSelector.js.map +1 -1
- package/lib/legend-ai/index.d.ts +4 -3
- package/lib/legend-ai/index.d.ts.map +1 -1
- package/lib/legend-ai/index.js +4 -3
- package/lib/legend-ai/index.js.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts +30 -13
- package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatProcessors.js +776 -216
- package/lib/legend-ai/stores/LegendAIChatProcessors.js.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatState.d.ts +2 -2
- package/lib/legend-ai/stores/LegendAIChatState.d.ts.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatState.js +26 -25
- package/lib/legend-ai/stores/LegendAIChatState.js.map +1 -1
- package/package.json +5 -5
- package/src/legend-ai/LegendAIDocEnrichment.ts +1198 -1
- package/src/legend-ai/LegendAIServiceRetrieval.ts +228 -0
- package/src/legend-ai/LegendAITypes.ts +138 -32
- package/src/legend-ai/LegendAI_LegendApplicationPlugin_Extension.ts +62 -0
- package/src/legend-ai/__test-utils__/LegendAITestUtils.ts +28 -0
- package/src/legend-ai/components/LegendAIAnalysisPanel.tsx +36 -17
- package/src/legend-ai/components/LegendAIAnalysisUtils.ts +178 -22
- package/src/legend-ai/components/LegendAICharts.tsx +160 -0
- package/src/legend-ai/components/LegendAIChat.tsx +107 -14
- package/src/legend-ai/components/LegendAIChatHelpers.ts +334 -20
- package/src/legend-ai/components/LegendAIChatInput.tsx +2 -1
- package/src/legend-ai/components/LegendAIScopeSelector.tsx +7 -2
- package/src/legend-ai/index.ts +22 -1
- package/src/legend-ai/stores/LegendAIChatProcessors.ts +1205 -321
- package/src/legend-ai/stores/LegendAIChatState.ts +80 -69
- package/tsconfig.json +1 -0
|
@@ -13,23 +13,30 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import { assertErrorThrown, uuid } from '@finos/legend-shared';
|
|
17
|
-
import {
|
|
16
|
+
import { assertErrorThrown, isPlainObject, uuid } from '@finos/legend-shared';
|
|
17
|
+
import { LegendAIQuestionIntent, LegendAIThinkingStepStatus, LegendAIMessageRole, LegendAIErrorType, LegendAIServiceError, LegendAIUnsupportedEngineShapeError, TDSServiceSourceType, buildColumnDefsFromNames, LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID, 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';
|
|
21
|
+
import { isFuzzyMatch, preFilterServicesByRelevance, } from '../LegendAIServiceRetrieval.js';
|
|
22
|
+
import { isNumericColumn, isStringColumn, } from '../components/LegendAIChatHelpers.js';
|
|
20
23
|
const MAX_ERROR_MESSAGE_LENGTH = 500;
|
|
21
24
|
const MAX_THINKING_ERROR_PREVIEW_LENGTH = 200;
|
|
22
25
|
const DEFAULT_MAX_JUDGE_ATTEMPTS = 5;
|
|
23
26
|
const DEFAULT_MAX_EXECUTION_RETRIES = 3;
|
|
24
27
|
const ANALYSIS_TIMEOUT_MS = 15_000;
|
|
28
|
+
const ORCHESTRATOR_GENERATION_TIMEOUT_MS = 120_000;
|
|
29
|
+
const EXECUTION_TIMEOUT_MS = 300_000;
|
|
25
30
|
const ANALYSIS_PREVIEW_ROW_LIMIT = 3;
|
|
26
31
|
const ANALYSIS_PREVIEW_VALUE_LIMIT = 40;
|
|
27
32
|
const MAX_NON_SQL_PASS_ATTEMPTS = 2;
|
|
28
|
-
const ALIAS_DOT_COL_PATTERN = /\b(?<tbl>[a-z]\w*)\s*\.\s*"(?<col>[^"]+)"/gi;
|
|
29
33
|
const JOIN_PATTERN = /\bJOIN\b/i;
|
|
30
34
|
const ORDER_BY_SPLIT = /\bORDER\s+BY\b/i;
|
|
31
35
|
const UNION_ALL_PATTERN = /\bUNION\s+ALL\b/i;
|
|
32
36
|
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
|
+
const SERVICE_CALL_PATTERN = /\bservice\s*\([^()]*\)/gi;
|
|
33
40
|
const DEFAULT_SAFETY_LIMIT = 1000;
|
|
34
41
|
const HAS_LIMIT_PATTERN = /\bLIMIT\s+\d+/i;
|
|
35
42
|
const HAS_AGGREGATION_PATTERN = /\bGROUP\s+BY\b|\bCOUNT\s*\(|\bSUM\s*\(|\bAVG\s*\(|\bMIN\s*\(|\bMAX\s*\(/i;
|
|
@@ -42,6 +49,7 @@ const SERVICE_PARAM_DATE_LIKE_PATTERNS = [
|
|
|
42
49
|
/effective|valid|settle|trade|maturity|expir|inception|close|open/i,
|
|
43
50
|
/start|end|from|until|begin|report|cutoff|valuation|pricing/i,
|
|
44
51
|
];
|
|
52
|
+
const GENERIC_TABLE_PATTERNS = /combined|consolidated|all|master|summary/i;
|
|
45
53
|
function isLikelySqlQuery(text) {
|
|
46
54
|
const trimmed = text.trim().toLowerCase();
|
|
47
55
|
return (trimmed.startsWith('select') ||
|
|
@@ -93,6 +101,7 @@ export function createMessagePair(text) {
|
|
|
93
101
|
isExecuting: false,
|
|
94
102
|
suggestedQueries: [],
|
|
95
103
|
fallbackAction: null,
|
|
104
|
+
queriedAccessPointGroups: [],
|
|
96
105
|
},
|
|
97
106
|
];
|
|
98
107
|
}
|
|
@@ -130,6 +139,54 @@ export function classifyError(error) {
|
|
|
130
139
|
}
|
|
131
140
|
return LegendAIErrorType.GENERAL;
|
|
132
141
|
}
|
|
142
|
+
export var ExecutionErrorCategory;
|
|
143
|
+
(function (ExecutionErrorCategory) {
|
|
144
|
+
ExecutionErrorCategory["INFRASTRUCTURE"] = "INFRASTRUCTURE";
|
|
145
|
+
ExecutionErrorCategory["SQL_FIXABLE"] = "SQL_FIXABLE";
|
|
146
|
+
ExecutionErrorCategory["ACCESS"] = "ACCESS";
|
|
147
|
+
ExecutionErrorCategory["NONE"] = "NONE";
|
|
148
|
+
})(ExecutionErrorCategory || (ExecutionErrorCategory = {}));
|
|
149
|
+
const EXECUTION_ERROR_RULES = [
|
|
150
|
+
{
|
|
151
|
+
category: ExecutionErrorCategory.ACCESS,
|
|
152
|
+
patterns: [
|
|
153
|
+
/\binsufficient privileges\b/i,
|
|
154
|
+
/\baccess denied\b/i,
|
|
155
|
+
/\bpermission denied\b/i,
|
|
156
|
+
/\bunauthorized\b/i,
|
|
157
|
+
/\bentitlement\b/i,
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
category: ExecutionErrorCategory.INFRASTRUCTURE,
|
|
162
|
+
patterns: [/__lake_action/i, /__lake/i],
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
category: ExecutionErrorCategory.SQL_FIXABLE,
|
|
166
|
+
patterns: [
|
|
167
|
+
/\binvalid identifier\b/i,
|
|
168
|
+
/\bambiguous column\b/i,
|
|
169
|
+
/\bdoes not exist\b/i,
|
|
170
|
+
/\bsql compilation error\b/i,
|
|
171
|
+
/\bnot found\b/i,
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
export function categorizeExecutionError(errMsg, error) {
|
|
176
|
+
if (error instanceof LegendAIUnsupportedEngineShapeError) {
|
|
177
|
+
return ExecutionErrorCategory.SQL_FIXABLE;
|
|
178
|
+
}
|
|
179
|
+
if (error instanceof LegendAIServiceError &&
|
|
180
|
+
error.errorType === LegendAIErrorType.PERMISSION) {
|
|
181
|
+
return ExecutionErrorCategory.ACCESS;
|
|
182
|
+
}
|
|
183
|
+
for (const rule of EXECUTION_ERROR_RULES) {
|
|
184
|
+
if (rule.patterns.some((pattern) => pattern.test(errMsg))) {
|
|
185
|
+
return rule.category;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return ExecutionErrorCategory.NONE;
|
|
189
|
+
}
|
|
133
190
|
export function finishWithThinkingError(setMessages, errorMsg, startTime, errorType) {
|
|
134
191
|
updateLastAssistant(setMessages, (msg) => ({
|
|
135
192
|
thinkingSteps: msg.thinkingSteps.map((s) => s.status === LegendAIThinkingStepStatus.ACTIVE
|
|
@@ -148,7 +205,10 @@ function buildTurnFromAssistant(userText, asstMsg) {
|
|
|
148
205
|
sql: asstMsg.sql,
|
|
149
206
|
intent: LegendAIQuestionIntent.DATA_QUERY,
|
|
150
207
|
};
|
|
151
|
-
if (asstMsg.
|
|
208
|
+
if (asstMsg.error) {
|
|
209
|
+
turn.resultSummary = `ERROR: ${asstMsg.error.slice(0, 200)}`;
|
|
210
|
+
}
|
|
211
|
+
else if (asstMsg.gridData) {
|
|
152
212
|
turn.rowCount = asstMsg.gridData.rowData.length;
|
|
153
213
|
const colNames = asstMsg.gridData.columnDefs
|
|
154
214
|
.map((c) => c.headerName ?? c.colId ?? '')
|
|
@@ -166,6 +226,14 @@ function buildTurnFromAssistant(userText, asstMsg) {
|
|
|
166
226
|
intent: LegendAIQuestionIntent.METADATA,
|
|
167
227
|
};
|
|
168
228
|
}
|
|
229
|
+
if (asstMsg.error && !asstMsg.sql) {
|
|
230
|
+
return {
|
|
231
|
+
question: userText,
|
|
232
|
+
sql: '(generation failed)',
|
|
233
|
+
resultSummary: `ERROR: ${asstMsg.error.slice(0, 200)}`,
|
|
234
|
+
intent: LegendAIQuestionIntent.DATA_QUERY,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
169
237
|
return undefined;
|
|
170
238
|
}
|
|
171
239
|
export function buildConversationHistory(messages) {
|
|
@@ -224,7 +292,21 @@ export function buildGenerationFailureMessage(failure, suggestion, services) {
|
|
|
224
292
|
return parts.join('');
|
|
225
293
|
}
|
|
226
294
|
function buildFallbackSuggestions(services) {
|
|
227
|
-
|
|
295
|
+
const result = [];
|
|
296
|
+
for (const svc of services.slice(0, 3)) {
|
|
297
|
+
const strCol = svc.columns.find(isStringColumn);
|
|
298
|
+
const numCol = svc.columns.find(isNumericColumn);
|
|
299
|
+
if (numCol && strCol) {
|
|
300
|
+
result.push(`What are the top 10 ${strCol.name} values by ${numCol.name} in ${svc.title}?`);
|
|
301
|
+
}
|
|
302
|
+
else if (strCol) {
|
|
303
|
+
result.push(`Show the breakdown by ${strCol.name} in ${svc.title}`);
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
result.push(`Show 10 records from ${svc.title}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return result;
|
|
228
310
|
}
|
|
229
311
|
function appendFallbackSuggestions(setMessages, services) {
|
|
230
312
|
const suggestions = buildFallbackSuggestions(services);
|
|
@@ -234,6 +316,28 @@ function appendFallbackSuggestions(setMessages, services) {
|
|
|
234
316
|
}));
|
|
235
317
|
}
|
|
236
318
|
}
|
|
319
|
+
function buildSnowflakeSqlError(rawError, services) {
|
|
320
|
+
const lowerError = rawError.toLowerCase();
|
|
321
|
+
const coreMatch = /SnowflakeSQLException:\s*(?<core>SQL compilation error[^"]*?)(?:\\n|\n|$)/i.exec(rawError);
|
|
322
|
+
const coreMsg = coreMatch?.groups?.core?.trim() ?? 'SQL compilation error';
|
|
323
|
+
if (lowerError.includes('__lake_action') || lowerError.includes('__lake')) {
|
|
324
|
+
return [
|
|
325
|
+
`Lakehouse SQL execution failed: ${coreMsg}`,
|
|
326
|
+
'\nThis is an internal engine error with this access point — it may not be fully supported yet.',
|
|
327
|
+
'\nTry querying a different access point, or contact the data product team.',
|
|
328
|
+
].join('');
|
|
329
|
+
}
|
|
330
|
+
if (lowerError.includes('invalid identifier') ||
|
|
331
|
+
lowerError.includes('does not exist')) {
|
|
332
|
+
const svcCols = services.map((s) => `${s.title}: ${s.columns.map((c) => c.name).join(', ')}`);
|
|
333
|
+
return [
|
|
334
|
+
`Lakehouse SQL execution failed: ${coreMsg}`,
|
|
335
|
+
`\nAvailable columns:\n${svcCols.join('\n')}`,
|
|
336
|
+
'\nTry rephrasing your question so the AI can pick the correct columns.',
|
|
337
|
+
].join('');
|
|
338
|
+
}
|
|
339
|
+
return `Lakehouse SQL execution failed: ${coreMsg}`;
|
|
340
|
+
}
|
|
237
341
|
export function buildExecutionErrorMessage(errStr, services) {
|
|
238
342
|
const errParts = [];
|
|
239
343
|
const errLower = errStr.toLowerCase();
|
|
@@ -251,6 +355,23 @@ export function buildExecutionErrorMessage(errStr, services) {
|
|
|
251
355
|
}
|
|
252
356
|
return errParts.join('');
|
|
253
357
|
}
|
|
358
|
+
if (errLower.includes('rename(~')) {
|
|
359
|
+
return [
|
|
360
|
+
'Cross-access-point JOINs on columns with the same name are not yet supported by the execution engine.',
|
|
361
|
+
'\nTry querying each access point separately, or ask a metadata question instead.',
|
|
362
|
+
].join('');
|
|
363
|
+
}
|
|
364
|
+
if (/can't find a match for function '\w+\(Timestamp/i.test(errStr) &&
|
|
365
|
+
errLower.includes('string')) {
|
|
366
|
+
return [
|
|
367
|
+
'Date comparison type mismatch: a Timestamp column was compared with a plain string literal.',
|
|
368
|
+
'\nPlease rephrase your question so the AI can regenerate the query with the correct date format.',
|
|
369
|
+
].join('');
|
|
370
|
+
}
|
|
371
|
+
// Snowflake SQL compilation errors — extract the core message from the trace
|
|
372
|
+
if (errLower.includes('snowflakesqlexception: sql compilation error')) {
|
|
373
|
+
return buildSnowflakeSqlError(errStr, services);
|
|
374
|
+
}
|
|
254
375
|
errParts.push(errStr.slice(0, MAX_ERROR_MESSAGE_LENGTH));
|
|
255
376
|
if (errLower.includes('column') &&
|
|
256
377
|
(errLower.includes('not found') ||
|
|
@@ -281,11 +402,54 @@ function parseSuggestedQueries(rawAnswer) {
|
|
|
281
402
|
.slice(0, 3);
|
|
282
403
|
return { answer, suggestedQueries };
|
|
283
404
|
}
|
|
405
|
+
const COVERAGE_STOPWORDS = new Set([
|
|
406
|
+
'show',
|
|
407
|
+
'give',
|
|
408
|
+
'join',
|
|
409
|
+
'combine',
|
|
410
|
+
'merge',
|
|
411
|
+
'columns',
|
|
412
|
+
'column',
|
|
413
|
+
'results',
|
|
414
|
+
'result',
|
|
415
|
+
'data',
|
|
416
|
+
'services',
|
|
417
|
+
'service',
|
|
418
|
+
'access',
|
|
419
|
+
'points',
|
|
420
|
+
'point',
|
|
421
|
+
'table',
|
|
422
|
+
'tables',
|
|
423
|
+
'query',
|
|
424
|
+
'rows',
|
|
425
|
+
'records',
|
|
426
|
+
'values',
|
|
427
|
+
'list',
|
|
428
|
+
'display',
|
|
429
|
+
'fetch',
|
|
430
|
+
'retrieve',
|
|
431
|
+
'return',
|
|
432
|
+
'from',
|
|
433
|
+
'with',
|
|
434
|
+
'that',
|
|
435
|
+
'this',
|
|
436
|
+
'what',
|
|
437
|
+
'which',
|
|
438
|
+
'where',
|
|
439
|
+
'have',
|
|
440
|
+
'each',
|
|
441
|
+
'both',
|
|
442
|
+
'first',
|
|
443
|
+
'last',
|
|
444
|
+
'some',
|
|
445
|
+
'available',
|
|
446
|
+
'possible',
|
|
447
|
+
]);
|
|
284
448
|
function tokenizeQuestionForCoverage(question) {
|
|
285
|
-
return question
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
449
|
+
return tokenizeText(question, {
|
|
450
|
+
minLength: 4,
|
|
451
|
+
stopwords: COVERAGE_STOPWORDS,
|
|
452
|
+
});
|
|
289
453
|
}
|
|
290
454
|
function buildQuestionCoverageNote(question, columns) {
|
|
291
455
|
const questionTokens = tokenizeQuestionForCoverage(question);
|
|
@@ -309,13 +473,44 @@ function formatPreviewValue(value) {
|
|
|
309
473
|
}
|
|
310
474
|
return `${raw.slice(0, ANALYSIS_PREVIEW_VALUE_LIMIT)}...`;
|
|
311
475
|
}
|
|
312
|
-
function
|
|
476
|
+
function buildJoinDiagnosticNote(query, columns, rows) {
|
|
477
|
+
if (!/\bJOIN\b/i.test(query) || rows.length === 0 || columns.length < 2) {
|
|
478
|
+
return '';
|
|
479
|
+
}
|
|
480
|
+
const allNullColumns = [];
|
|
481
|
+
const someDataColumns = [];
|
|
482
|
+
for (const col of columns) {
|
|
483
|
+
const allNull = rows.every((row) => {
|
|
484
|
+
if (isPlainObject(row)) {
|
|
485
|
+
const val = row[col];
|
|
486
|
+
return val === null || val === undefined;
|
|
487
|
+
}
|
|
488
|
+
return true;
|
|
489
|
+
});
|
|
490
|
+
if (allNull) {
|
|
491
|
+
allNullColumns.push(col);
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
someDataColumns.push(col);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (allNullColumns.length > 0 &&
|
|
498
|
+
someDataColumns.length > 0 &&
|
|
499
|
+
allNullColumns.length >= 2) {
|
|
500
|
+
return (`Note: ${allNullColumns.length} columns (${allNullColumns.slice(0, 3).join(', ')}${allNullColumns.length > 3 ? ', ...' : ''}) ` +
|
|
501
|
+
'returned entirely NULL values. This typically means the joined services do not share ' +
|
|
502
|
+
'overlapping values for the join key. The services may track different records. ' +
|
|
503
|
+
'Try querying each service separately to explore their data independently.');
|
|
504
|
+
}
|
|
505
|
+
return '';
|
|
506
|
+
}
|
|
507
|
+
function buildDeterministicResultSummary(question, query, columns, rows) {
|
|
313
508
|
const selectedColumns = columns.length === 0 ? 'none' : columns.join(', ');
|
|
314
509
|
const rowCount = rows.length;
|
|
315
510
|
const previewRows = rows
|
|
316
511
|
.slice(0, ANALYSIS_PREVIEW_ROW_LIMIT)
|
|
317
512
|
.map((row, index) => {
|
|
318
|
-
if (
|
|
513
|
+
if (isPlainObject(row)) {
|
|
319
514
|
const entries = Object.entries(row).slice(0, 4);
|
|
320
515
|
const formattedEntries = entries.map(([key, value]) => `${key}: ${formatPreviewValue(value)}`);
|
|
321
516
|
return `${index + 1}. ${formattedEntries.join(', ')}`;
|
|
@@ -324,11 +519,15 @@ function buildDeterministicResultSummary(question, _query, columns, rows) {
|
|
|
324
519
|
})
|
|
325
520
|
.join('\n');
|
|
326
521
|
const coverageNote = buildQuestionCoverageNote(question, columns);
|
|
522
|
+
const joinDiagnostic = buildJoinDiagnosticNote(query, columns, rows);
|
|
327
523
|
const parts = [
|
|
328
524
|
`I retrieved ${rowCount} row${rowCount === 1 ? '' : 's'} for your question using this query.`,
|
|
329
525
|
`Columns returned: ${selectedColumns}.`,
|
|
330
526
|
`Sample rows:\n${previewRows || 'No sample rows available.'}`,
|
|
331
527
|
];
|
|
528
|
+
if (joinDiagnostic) {
|
|
529
|
+
parts.push(joinDiagnostic);
|
|
530
|
+
}
|
|
332
531
|
if (coverageNote) {
|
|
333
532
|
parts.push(coverageNote);
|
|
334
533
|
}
|
|
@@ -363,10 +562,10 @@ export function attachMetadataOverview(setMessages, metadataOverview) {
|
|
|
363
562
|
};
|
|
364
563
|
});
|
|
365
564
|
}
|
|
366
|
-
export async function handleMetadataQuestion(question, metadata, context, startTime, hasQueryableServices) {
|
|
565
|
+
export async function handleMetadataQuestion(question, metadata, context, startTime, hasQueryableServices, services, modelContextEnrichment) {
|
|
367
566
|
const { config, plugin, history, setMessages } = context;
|
|
368
567
|
addThinkingStep(setMessages, 'Answering from product metadata...');
|
|
369
|
-
const metadataPromptText = plugin.buildMetadataPrompt(question, metadata, history);
|
|
568
|
+
const metadataPromptText = plugin.buildMetadataPrompt(question, metadata, history, services, modelContextEnrichment);
|
|
370
569
|
const rawAnswer = await plugin.callLLM(metadataPromptText, config);
|
|
371
570
|
const { answer, suggestedQueries: parsedSuggestions } = parseSuggestedQueries(rawAnswer);
|
|
372
571
|
const suggestedQueries = hasQueryableServices === false && !config.orchestratorUrl
|
|
@@ -433,10 +632,10 @@ async function runJudgeLoop(generatedSql, buildJudgePromptFn, context) {
|
|
|
433
632
|
}
|
|
434
633
|
return null;
|
|
435
634
|
}
|
|
436
|
-
export async function generateAndJudgeSql(question, services, coordinates, context, startTime, metadata) {
|
|
635
|
+
export async function generateAndJudgeSql(question, services, coordinates, context, startTime, metadata, modelContextEnrichment) {
|
|
437
636
|
const { plugin, config, history, setMessages } = context;
|
|
438
637
|
addThinkingStep(setMessages, 'Building context from service schemas...');
|
|
439
|
-
const prompt = plugin.buildGeneratorPrompt(question, services, coordinates, history, metadata);
|
|
638
|
+
const prompt = plugin.buildGeneratorPrompt(question, services, coordinates, history, metadata, modelContextEnrichment);
|
|
440
639
|
addThinkingStep(setMessages, 'Generating SQL query...');
|
|
441
640
|
const answerText = await plugin.callLLM(prompt, config);
|
|
442
641
|
const { sql: generatedSql, failure, suggestion, } = plugin.extractSqlFromResponse(answerText);
|
|
@@ -457,10 +656,11 @@ export async function generateAndJudgeSql(question, services, coordinates, conte
|
|
|
457
656
|
* Uses AP-specific prompts that focus on `p()` syntax and omit
|
|
458
657
|
* coordinates, parameters, and service()-related rules.
|
|
459
658
|
*/
|
|
460
|
-
export async function generateAndJudgeAccessPointSql(question, accessPoints, context, startTime) {
|
|
659
|
+
export async function generateAndJudgeAccessPointSql(question, accessPoints, context, startTime, modelContextEnrichment) {
|
|
461
660
|
const { plugin, config, history, setMessages } = context;
|
|
462
661
|
addThinkingStep(setMessages, 'Building context from access point schemas...');
|
|
463
|
-
|
|
662
|
+
plugin.preWarmSchemaAnalysis(accessPoints, config);
|
|
663
|
+
const prompt = plugin.buildAccessPointGeneratorPrompt(question, accessPoints, history, modelContextEnrichment);
|
|
464
664
|
addThinkingStep(setMessages, 'Generating SQL query for access points...');
|
|
465
665
|
const answerText = await plugin.callLLM(prompt, config);
|
|
466
666
|
const { sql: generatedSql, failure, suggestion, } = plugin.extractSqlFromResponse(answerText);
|
|
@@ -474,7 +674,7 @@ export async function generateAndJudgeAccessPointSql(question, accessPoints, con
|
|
|
474
674
|
finishWithThinkingError(setMessages, 'Could not extract SQL from LLM response.\nTry rephrasing your question or ask about a specific access point.', startTime, LegendAIErrorType.GENERATION);
|
|
475
675
|
return null;
|
|
476
676
|
}
|
|
477
|
-
return runJudgeLoop(generatedSql, (sql) => plugin.buildAccessPointJudgePrompt(sql, question, accessPoints, history), context);
|
|
677
|
+
return runJudgeLoop(generatedSql, (sql) => plugin.buildAccessPointJudgePrompt(sql, question, accessPoints, history, modelContextEnrichment), context);
|
|
478
678
|
}
|
|
479
679
|
function reportExecutionResult(rawResult, setMessages, execStartTime, startTime) {
|
|
480
680
|
const columns = deduplicateColumns(rawResult.columns);
|
|
@@ -494,10 +694,7 @@ function reportExecutionResult(rawResult, setMessages, execStartTime, startTime)
|
|
|
494
694
|
export async function executeSqlAndReport(sql, services, config, plugin, setMessages, startTime, dataProductCoordinates) {
|
|
495
695
|
const execStartTime = Date.now();
|
|
496
696
|
try {
|
|
497
|
-
const
|
|
498
|
-
const rawResult = isAccessPoint && dataProductCoordinates
|
|
499
|
-
? await plugin.executeLakehouseSql(sql, dataProductCoordinates, config)
|
|
500
|
-
: await plugin.executeSql(sql, config);
|
|
697
|
+
const rawResult = await executeSqlForServices(sql, services, dataProductCoordinates, plugin, config);
|
|
501
698
|
return reportExecutionResult(rawResult, setMessages, execStartTime, startTime);
|
|
502
699
|
}
|
|
503
700
|
catch (executeError) {
|
|
@@ -514,11 +711,28 @@ export async function executeSqlAndReport(sql, services, config, plugin, setMess
|
|
|
514
711
|
return undefined;
|
|
515
712
|
}
|
|
516
713
|
}
|
|
714
|
+
const HAS_PURE_AGGREGATION_PATTERN = /->groupBy\(|->distinct\(|->count\(\)|->sum\(\)|->average\(\)|->olapGroupBy\(/;
|
|
715
|
+
const HAS_PURE_TAKE_PATTERN = /->take\(\s*\d+\s*\)/;
|
|
716
|
+
const PURE_LIMIT_PATTERN = /->limit\(\s*(?<count>\d+)\s*\)/g;
|
|
717
|
+
export function ensurePureSafetyLimit(pureQuery, limit = DEFAULT_SAFETY_LIMIT) {
|
|
718
|
+
const normalized = pureQuery.replaceAll(PURE_LIMIT_PATTERN, (_match, n) => `->take(${n})`);
|
|
719
|
+
if (HAS_PURE_TAKE_PATTERN.test(normalized)) {
|
|
720
|
+
return normalized;
|
|
721
|
+
}
|
|
722
|
+
if (HAS_PURE_AGGREGATION_PATTERN.test(normalized)) {
|
|
723
|
+
return normalized;
|
|
724
|
+
}
|
|
725
|
+
return `${normalized.trimEnd()}->take(${limit})`;
|
|
726
|
+
}
|
|
517
727
|
export async function executePureQueryAndReport(pureQuery, pureExecutionContext, dataProductCoordinates, config, plugin, setMessages, startTime) {
|
|
518
728
|
const execStartTime = Date.now();
|
|
729
|
+
const safeQuery = ensurePureSafetyLimit(pureQuery);
|
|
519
730
|
try {
|
|
520
731
|
addThinkingStep(setMessages, 'Executing Pure query...');
|
|
521
|
-
const rawResult = await plugin.executePureQuery(
|
|
732
|
+
const rawResult = await withTimeout(plugin.executePureQuery(safeQuery, pureExecutionContext, dataProductCoordinates, config), EXECUTION_TIMEOUT_MS);
|
|
733
|
+
if (!rawResult) {
|
|
734
|
+
throw new Error('Query execution timed out after 5 minutes. The dataset may be too large — try adding more filters or reducing the result set.');
|
|
735
|
+
}
|
|
522
736
|
return reportExecutionResult(rawResult, setMessages, execStartTime, startTime);
|
|
523
737
|
}
|
|
524
738
|
catch (executeError) {
|
|
@@ -536,7 +750,7 @@ export async function executePureQueryAndReport(pureQuery, pureExecutionContext,
|
|
|
536
750
|
: execErrorType,
|
|
537
751
|
thinkingDuration: elapsedSeconds(startTime),
|
|
538
752
|
}));
|
|
539
|
-
return
|
|
753
|
+
return undefined;
|
|
540
754
|
}
|
|
541
755
|
}
|
|
542
756
|
export async function analyzeOrchestratorResults(question, query, execResult, metadata, context, startTime) {
|
|
@@ -571,43 +785,66 @@ export async function analyzeOrchestratorResults(question, query, execResult, me
|
|
|
571
785
|
}));
|
|
572
786
|
}
|
|
573
787
|
}
|
|
574
|
-
async function
|
|
788
|
+
async function retryWithAlternateRoot(question, alternateRoot, resolvedEntities, orchestratorOptions, metadata, context, options) {
|
|
789
|
+
const { startTime, modelContext } = options;
|
|
575
790
|
const { dataProductCoordinates, pureExecutionContext } = orchestratorOptions;
|
|
576
791
|
const { config, plugin, setMessages } = context;
|
|
792
|
+
addThinkingStep(setMessages, `No results with ${extractElementNameFromPath(resolvedEntities.rootEntity)}, retrying with ${extractElementNameFromPath(alternateRoot)}...`);
|
|
793
|
+
try {
|
|
794
|
+
const retryRelated = resolvedEntities.relatedEntities.filter((e) => e !== alternateRoot);
|
|
795
|
+
const retryEnriched = modelContext
|
|
796
|
+
? buildEnrichedBusinessContext(question, alternateRoot, retryRelated, modelContext)
|
|
797
|
+
: undefined;
|
|
798
|
+
const retryResponse = await withTimeout(plugin.generateQueryViaOrchestrator({
|
|
799
|
+
user_question: question,
|
|
800
|
+
semantic_search_resolution_details: {
|
|
801
|
+
data_product_coordinates: dataProductCoordinates,
|
|
802
|
+
root_entity: alternateRoot,
|
|
803
|
+
related_entities: retryRelated,
|
|
804
|
+
...(retryEnriched
|
|
805
|
+
? { enriched_business_context: retryEnriched }
|
|
806
|
+
: {}),
|
|
807
|
+
},
|
|
808
|
+
}, config), ORCHESTRATOR_GENERATION_TIMEOUT_MS);
|
|
809
|
+
if (!retryResponse) {
|
|
810
|
+
addThinkingStep(setMessages, 'Retry query generation timed out');
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
const retrySafeQuery = ensurePureSafetyLimit(retryResponse.legend_query);
|
|
814
|
+
updateLastAssistant(setMessages, () => ({
|
|
815
|
+
sql: retrySafeQuery,
|
|
816
|
+
sqlGenTime: elapsedSeconds(startTime, 2),
|
|
817
|
+
isExecuting: true,
|
|
818
|
+
}));
|
|
819
|
+
const retryResult = await executePureQueryAndReport(retrySafeQuery, pureExecutionContext, dataProductCoordinates, config, plugin, setMessages, startTime);
|
|
820
|
+
if (retryResult && retryResult.rows.length > 0) {
|
|
821
|
+
await analyzeOrchestratorResults(question, retryResponse.legend_query, retryResult, metadata, context, startTime);
|
|
822
|
+
return true;
|
|
823
|
+
}
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
catch (retryError) {
|
|
827
|
+
assertErrorThrown(retryError);
|
|
828
|
+
addThinkingStep(setMessages, `Retry with alternate entity failed: ${retryError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
829
|
+
return false;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
async function handleEmptyOrchestratorResults(question, legendQuery, orchestratorOptions, metadata, resolvedEntities, context, options) {
|
|
833
|
+
const { startTime, modelContext } = options;
|
|
834
|
+
const { config, plugin, setMessages } = context;
|
|
577
835
|
if (resolvedEntities.relatedEntities.length > 0) {
|
|
578
|
-
const alternateRoot =
|
|
836
|
+
const alternateRoot = modelContext
|
|
837
|
+
? findBestAlternateRoot(resolvedEntities.rootEntity, resolvedEntities.relatedEntities, modelContext)
|
|
838
|
+
: resolvedEntities.relatedEntities[0];
|
|
579
839
|
if (alternateRoot) {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
user_question: question,
|
|
584
|
-
semantic_search_resolution_details: {
|
|
585
|
-
data_product_coordinates: dataProductCoordinates,
|
|
586
|
-
root_entity: alternateRoot,
|
|
587
|
-
related_entities: resolvedEntities.relatedEntities.slice(1),
|
|
588
|
-
},
|
|
589
|
-
}, config);
|
|
590
|
-
updateLastAssistant(setMessages, () => ({
|
|
591
|
-
sql: retryResponse.legend_query,
|
|
592
|
-
sqlGenTime: elapsedSeconds(startTime, 2),
|
|
593
|
-
isExecuting: true,
|
|
594
|
-
}));
|
|
595
|
-
const retryResult = await executePureQueryAndReport(retryResponse.legend_query, pureExecutionContext, dataProductCoordinates, config, plugin, setMessages, startTime);
|
|
596
|
-
if (retryResult.rows.length > 0) {
|
|
597
|
-
await analyzeOrchestratorResults(question, retryResponse.legend_query, retryResult, metadata, context, startTime);
|
|
598
|
-
return;
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
catch (retryError) {
|
|
602
|
-
assertErrorThrown(retryError);
|
|
603
|
-
addThinkingStep(setMessages, `Retry with alternate entity failed: ${retryError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
840
|
+
const succeeded = await retryWithAlternateRoot(question, alternateRoot, resolvedEntities, orchestratorOptions, metadata, context, { startTime, ...(modelContext === undefined ? {} : { modelContext }) });
|
|
841
|
+
if (succeeded) {
|
|
842
|
+
return;
|
|
604
843
|
}
|
|
605
844
|
}
|
|
606
845
|
}
|
|
607
846
|
addThinkingStep(setMessages, 'No results returned \u2014 building contextual guidance...');
|
|
608
|
-
updateLastAssistant(setMessages, () => ({
|
|
609
|
-
isProcessing: true,
|
|
610
|
-
}));
|
|
847
|
+
updateLastAssistant(setMessages, () => ({ isProcessing: true }));
|
|
611
848
|
const fallback = await withTimeout(plugin.buildNoResultsFallback(question, legendQuery, metadata, config), ANALYSIS_TIMEOUT_MS);
|
|
612
849
|
if (fallback) {
|
|
613
850
|
completeThinkingSteps(setMessages);
|
|
@@ -619,37 +856,88 @@ async function handleEmptyOrchestratorResults(question, legendQuery, orchestrato
|
|
|
619
856
|
}));
|
|
620
857
|
}
|
|
621
858
|
}
|
|
622
|
-
|
|
859
|
+
async function resolveOrchestrationEntities(question, dataProductCoordinates, context, pureExecutionContext, preResolvedEntities, modelContext) {
|
|
623
860
|
const { config, plugin, setMessages } = context;
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
861
|
+
if (preResolvedEntities) {
|
|
862
|
+
addThinkingStep(setMessages, `Using pre-resolved root entity: ${extractElementNameFromPath(preResolvedEntities.rootEntity)}`);
|
|
863
|
+
return preResolvedEntities;
|
|
864
|
+
}
|
|
865
|
+
addThinkingStep(setMessages, 'Resolving entities for your query...');
|
|
866
|
+
const resolvedEntities = await plugin.resolveEntitiesForQuery(question, dataProductCoordinates, config, pureExecutionContext, modelContext);
|
|
867
|
+
addThinkingStep(setMessages, `Found root entity: ${extractElementNameFromPath(resolvedEntities.rootEntity)}`);
|
|
868
|
+
return resolvedEntities;
|
|
869
|
+
}
|
|
870
|
+
function extractUserErrorFromMessage(errorMessage) {
|
|
871
|
+
const marker = 'with error:';
|
|
872
|
+
const idx = errorMessage.toLowerCase().indexOf(marker);
|
|
873
|
+
if (idx !== -1) {
|
|
874
|
+
const after = errorMessage.slice(idx + marker.length).trimStart();
|
|
875
|
+
const newlineIdx = after.indexOf('\n');
|
|
876
|
+
const reason = (newlineIdx === -1 ? after : after.slice(0, newlineIdx)).trim();
|
|
877
|
+
if (reason.length > 0) {
|
|
878
|
+
return reason.slice(0, MAX_ERROR_MESSAGE_LENGTH);
|
|
630
879
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
880
|
+
}
|
|
881
|
+
return errorMessage.slice(0, MAX_ERROR_MESSAGE_LENGTH);
|
|
882
|
+
}
|
|
883
|
+
async function handleOrchestratorError(error, startTime, question, metadata, context) {
|
|
884
|
+
const { config, plugin, setMessages } = context;
|
|
885
|
+
const orchErrorType = classifyError(error);
|
|
886
|
+
const genTime = elapsedSeconds(startTime, 2);
|
|
887
|
+
addThinkingStep(setMessages, `Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
888
|
+
try {
|
|
889
|
+
addThinkingStep(setMessages, 'Building guidance from available metadata...');
|
|
890
|
+
const fallbackText = await withTimeout(plugin.buildFailureFallback(question, error.message, metadata, config), ANALYSIS_TIMEOUT_MS);
|
|
891
|
+
if (fallbackText) {
|
|
892
|
+
completeThinkingSteps(setMessages);
|
|
893
|
+
updateLastAssistant(setMessages, () => ({
|
|
894
|
+
dataContext: fallbackText,
|
|
895
|
+
sqlGenTime: genTime,
|
|
896
|
+
error: extractUserErrorFromMessage(error.message),
|
|
897
|
+
errorType: LegendAIErrorType.GENERATION,
|
|
898
|
+
isProcessing: false,
|
|
899
|
+
thinkingDuration: elapsedSeconds(startTime),
|
|
900
|
+
}));
|
|
901
|
+
return;
|
|
635
902
|
}
|
|
903
|
+
}
|
|
904
|
+
catch (fallbackError) {
|
|
905
|
+
assertErrorThrown(fallbackError);
|
|
906
|
+
addThinkingStep(setMessages, `Fallback guidance failed: ${fallbackError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
907
|
+
}
|
|
908
|
+
finishWithThinkingError(setMessages, error.message, startTime, orchErrorType);
|
|
909
|
+
}
|
|
910
|
+
export async function processQuestionViaOrchestrator(question, dataProductCoordinates, metadata, context, pureExecutionContext, preResolvedEntities, modelContext) {
|
|
911
|
+
const { config, plugin, setMessages } = context;
|
|
912
|
+
const startTime = Date.now();
|
|
913
|
+
try {
|
|
914
|
+
const resolvedEntities = await resolveOrchestrationEntities(question, dataProductCoordinates, context, pureExecutionContext, preResolvedEntities, modelContext);
|
|
636
915
|
if (resolvedEntities.relatedEntities.length > 0) {
|
|
637
916
|
addThinkingStep(setMessages, `Found ${resolvedEntities.relatedEntities.length} related entities`);
|
|
638
917
|
}
|
|
639
918
|
addThinkingStep(setMessages, 'Generating Legend query via orchestrator...');
|
|
640
|
-
const
|
|
919
|
+
const enrichedContext = modelContext
|
|
920
|
+
? buildEnrichedBusinessContext(question, resolvedEntities.rootEntity, resolvedEntities.relatedEntities, modelContext)
|
|
921
|
+
: undefined;
|
|
922
|
+
const orchestratorResponse = await withTimeout(plugin.generateQueryViaOrchestrator({
|
|
641
923
|
user_question: question,
|
|
642
924
|
semantic_search_resolution_details: {
|
|
643
925
|
data_product_coordinates: dataProductCoordinates,
|
|
644
926
|
root_entity: resolvedEntities.rootEntity,
|
|
645
927
|
related_entities: resolvedEntities.relatedEntities,
|
|
928
|
+
...(enrichedContext
|
|
929
|
+
? { enriched_business_context: enrichedContext }
|
|
930
|
+
: {}),
|
|
646
931
|
},
|
|
647
|
-
}, config);
|
|
648
|
-
|
|
932
|
+
}, config), ORCHESTRATOR_GENERATION_TIMEOUT_MS);
|
|
933
|
+
if (!orchestratorResponse) {
|
|
934
|
+
throw new Error('Query generation timed out. The orchestrator took too long to respond. Try a simpler question.');
|
|
935
|
+
}
|
|
936
|
+
const safeQuery = ensurePureSafetyLimit(orchestratorResponse.legend_query);
|
|
649
937
|
completeThinkingSteps(setMessages);
|
|
650
938
|
updateLastAssistant(setMessages, () => ({
|
|
651
|
-
sql:
|
|
652
|
-
sqlGenTime:
|
|
939
|
+
sql: safeQuery,
|
|
940
|
+
sqlGenTime: elapsedSeconds(startTime, 2),
|
|
653
941
|
isExecuting: true,
|
|
654
942
|
isProcessing: true,
|
|
655
943
|
}));
|
|
@@ -663,13 +951,19 @@ export async function processQuestionViaOrchestrator(question, dataProductCoordi
|
|
|
663
951
|
}));
|
|
664
952
|
return;
|
|
665
953
|
}
|
|
666
|
-
const execResult = await executePureQueryAndReport(
|
|
954
|
+
const execResult = await executePureQueryAndReport(safeQuery, pureExecutionContext, dataProductCoordinates, config, plugin, setMessages, startTime);
|
|
955
|
+
if (!execResult) {
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
667
958
|
try {
|
|
668
959
|
if (execResult.rows.length > 0) {
|
|
669
|
-
await analyzeOrchestratorResults(question,
|
|
960
|
+
await analyzeOrchestratorResults(question, safeQuery, execResult, metadata, context, startTime);
|
|
670
961
|
}
|
|
671
962
|
else {
|
|
672
|
-
await handleEmptyOrchestratorResults(question,
|
|
963
|
+
await handleEmptyOrchestratorResults(question, safeQuery, { dataProductCoordinates, pureExecutionContext }, metadata, resolvedEntities, context, {
|
|
964
|
+
startTime,
|
|
965
|
+
...(modelContext === undefined ? {} : { modelContext }),
|
|
966
|
+
});
|
|
673
967
|
}
|
|
674
968
|
}
|
|
675
969
|
catch (analysisError) {
|
|
@@ -686,26 +980,7 @@ export async function processQuestionViaOrchestrator(question, dataProductCoordi
|
|
|
686
980
|
}
|
|
687
981
|
catch (error) {
|
|
688
982
|
assertErrorThrown(error);
|
|
689
|
-
|
|
690
|
-
addThinkingStep(setMessages, `Error: ${error.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
691
|
-
try {
|
|
692
|
-
addThinkingStep(setMessages, 'Building guidance from available metadata...');
|
|
693
|
-
const fallbackText = await withTimeout(plugin.buildFailureFallback(question, error.message, metadata, config), ANALYSIS_TIMEOUT_MS);
|
|
694
|
-
if (fallbackText) {
|
|
695
|
-
completeThinkingSteps(setMessages);
|
|
696
|
-
updateLastAssistant(setMessages, () => ({
|
|
697
|
-
dataContext: fallbackText,
|
|
698
|
-
isProcessing: false,
|
|
699
|
-
thinkingDuration: elapsedSeconds(startTime),
|
|
700
|
-
}));
|
|
701
|
-
return;
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
catch (fallbackError) {
|
|
705
|
-
assertErrorThrown(fallbackError);
|
|
706
|
-
addThinkingStep(setMessages, `Fallback guidance failed: ${fallbackError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
707
|
-
}
|
|
708
|
-
finishWithThinkingError(setMessages, error.message, startTime, orchErrorType);
|
|
983
|
+
await handleOrchestratorError(error, startTime, question, metadata, context);
|
|
709
984
|
}
|
|
710
985
|
}
|
|
711
986
|
export function cleanLlmSqlResponse(raw) {
|
|
@@ -732,9 +1007,7 @@ export function sanitizeJoinOrderBy(sql) {
|
|
|
732
1007
|
const beforeOrderBy = parts[0] ?? '';
|
|
733
1008
|
const afterOrderBy = parts.slice(1).join('ORDER BY').replace(/^\s+/, '');
|
|
734
1009
|
const selectAliases = new Map();
|
|
735
|
-
const
|
|
736
|
-
let m;
|
|
737
|
-
while ((m = aliasRegex.exec(beforeOrderBy)) !== null) {
|
|
1010
|
+
for (const m of beforeOrderBy.matchAll(SELECT_ALIAS_PATTERN)) {
|
|
738
1011
|
const tableAlias = (m.groups?.tbl ?? '').toLowerCase();
|
|
739
1012
|
const colName = (m.groups?.col ?? '').toLowerCase();
|
|
740
1013
|
const asAlias = m.groups?.qAlias ?? m.groups?.uAlias ?? '';
|
|
@@ -743,17 +1016,72 @@ export function sanitizeJoinOrderBy(sql) {
|
|
|
743
1016
|
if (selectAliases.size === 0) {
|
|
744
1017
|
return sql;
|
|
745
1018
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
1019
|
+
// Build a lookup of the original alias.col text → its SELECT alias replacement.
|
|
1020
|
+
// matchAll exposes typed `.groups`, so no cast is needed.
|
|
1021
|
+
const replacements = new Map();
|
|
1022
|
+
for (const m of afterOrderBy.matchAll(ALIAS_DOT_COL_PATTERN)) {
|
|
1023
|
+
const tbl = (m.groups?.tbl ?? '').toLowerCase();
|
|
1024
|
+
const col = (m.groups?.col ?? '').toLowerCase();
|
|
1025
|
+
const alias = selectAliases.get(`${tbl}.${col}`);
|
|
1026
|
+
if (alias) {
|
|
1027
|
+
replacements.set(m[0], `"${alias}"`);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
if (replacements.size === 0) {
|
|
1031
|
+
return sql;
|
|
1032
|
+
}
|
|
1033
|
+
const rewritten = afterOrderBy.replaceAll(ALIAS_DOT_COL_PATTERN, (match) => replacements.get(match) ?? match);
|
|
752
1034
|
if (rewritten === afterOrderBy) {
|
|
753
1035
|
return sql;
|
|
754
1036
|
}
|
|
755
1037
|
return `${beforeOrderBy}ORDER BY ${rewritten}`;
|
|
756
1038
|
}
|
|
1039
|
+
export function sanitizeJoinSameKeyColumns(sql, services) {
|
|
1040
|
+
if (!JOIN_PATTERN.test(sql)) {
|
|
1041
|
+
return sql;
|
|
1042
|
+
}
|
|
1043
|
+
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;
|
|
1044
|
+
let result = sql;
|
|
1045
|
+
let match;
|
|
1046
|
+
while ((match = joinRegex.exec(sql)) !== null) {
|
|
1047
|
+
const pId = match.groups?.pId ?? '';
|
|
1048
|
+
const rightAlias = match.groups?.rAlias ?? '';
|
|
1049
|
+
const onClause = (match.groups?.onClause ?? '').slice(0, 500);
|
|
1050
|
+
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);
|
|
1051
|
+
if (!sameKeyMatch) {
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
1054
|
+
const leftCol = sameKeyMatch.groups?.lCol ?? '';
|
|
1055
|
+
const rightCol = sameKeyMatch.groups?.rCol ?? '';
|
|
1056
|
+
const matchedRightAlias = sameKeyMatch.groups?.rAl ?? '';
|
|
1057
|
+
if (leftCol.toLowerCase() !== rightCol.toLowerCase() ||
|
|
1058
|
+
matchedRightAlias.toLowerCase() !== rightAlias.toLowerCase()) {
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
const renamedKey = `${rightAlias}_${rightCol}`;
|
|
1062
|
+
const columnList = buildSubqueryColumnList(pId, rightCol, renamedKey, services);
|
|
1063
|
+
const originalFragment = `p('${pId}') AS ${rightAlias}`;
|
|
1064
|
+
const subqueryFragment = `(SELECT ${columnList} FROM p('${pId}')) AS ${rightAlias}`;
|
|
1065
|
+
const oldOnRef = `${rightAlias}."${rightCol}"`;
|
|
1066
|
+
const newOnRef = `${rightAlias}."${renamedKey}"`;
|
|
1067
|
+
result = result.replace(originalFragment, subqueryFragment);
|
|
1068
|
+
result = result.replaceAll(oldOnRef, newOnRef);
|
|
1069
|
+
}
|
|
1070
|
+
return result;
|
|
1071
|
+
}
|
|
1072
|
+
function buildSubqueryColumnList(pId, originalKey, renamedKey, services) {
|
|
1073
|
+
const svc = services?.find((s) => s.dataProductPath &&
|
|
1074
|
+
s.pattern &&
|
|
1075
|
+
pId === `${s.dataProductPath}.${s.pattern.replace(/^\//, '')}`);
|
|
1076
|
+
if (!svc || svc.columns.length === 0) {
|
|
1077
|
+
return `"${originalKey}" AS "${renamedKey}", *`;
|
|
1078
|
+
}
|
|
1079
|
+
return svc.columns
|
|
1080
|
+
.map((c) => c.name.toLowerCase() === originalKey.toLowerCase()
|
|
1081
|
+
? `"${c.name}" AS "${renamedKey}"`
|
|
1082
|
+
: `"${c.name}"`)
|
|
1083
|
+
.join(', ');
|
|
1084
|
+
}
|
|
757
1085
|
export function sanitizeLiteralColumns(sql) {
|
|
758
1086
|
if (!UNION_ALL_PATTERN.test(sql)) {
|
|
759
1087
|
return sql;
|
|
@@ -817,8 +1145,7 @@ export function ensureDateParameters(sql, services) {
|
|
|
817
1145
|
return sql;
|
|
818
1146
|
}
|
|
819
1147
|
const today = getTodayISO();
|
|
820
|
-
|
|
821
|
-
return sql.replaceAll(serviceCallPattern, (match) => {
|
|
1148
|
+
return sql.replaceAll(SERVICE_CALL_PATTERN, (match) => {
|
|
822
1149
|
let patched = match;
|
|
823
1150
|
for (const param of dateParams) {
|
|
824
1151
|
if (new RegExp(String.raw `\b${param}\s*=>`, 'i').test(patched)) {
|
|
@@ -897,6 +1224,84 @@ export function buildMissingParamsWarning(missingParams) {
|
|
|
897
1224
|
`Please provide ${missingParams.length > 1 ? 'values' : 'a value'} in your question, e.g. "show data where ${exampleValues}".`,
|
|
898
1225
|
].join('\n');
|
|
899
1226
|
}
|
|
1227
|
+
const NESTED_P_IN_CLAUSE = /\bIN\s*\(\s*SELECT\b[^)]*\bFROM\s+p\s*\(/i;
|
|
1228
|
+
const NESTED_P_CROSS_JOIN = /\bCROSS\s+JOIN\s*\(\s*SELECT\b[^)]*\bFROM\s+p\s*\(/i;
|
|
1229
|
+
const NESTED_P_SCALAR = /\(\s*SELECT\s+(?:COUNT|SUM|AVG|MIN|MAX)\s*\([^)]*\)\s+(?:AS\s+\w+\s+)?FROM\s+p\s*\(/i;
|
|
1230
|
+
export function hasNestedPCalls(sql) {
|
|
1231
|
+
return (NESTED_P_IN_CLAUSE.test(sql) ||
|
|
1232
|
+
NESTED_P_CROSS_JOIN.test(sql) ||
|
|
1233
|
+
NESTED_P_SCALAR.test(sql));
|
|
1234
|
+
}
|
|
1235
|
+
const NESTED_AGGREGATE_PATTERN = /\b(?:SUM|AVG|MIN|MAX|COUNT)\s*\(\s*(?:SUM|AVG|MIN|MAX|COUNT)\s*\(/i;
|
|
1236
|
+
const OVER_CLAUSE_PATTERN = /\bOVER\s*\(/i;
|
|
1237
|
+
const AGGREGATE_IN_WINDOW_ARGS_PATTERN = /\bOVER\s*\((?:[^()]|\([^()]*\)){0,300}\b(?:SUM|AVG|MIN|MAX|COUNT)\s*\(/i;
|
|
1238
|
+
const NON_WINDOW_SCALAR_FUNCTIONS = new Set([
|
|
1239
|
+
'ROUND',
|
|
1240
|
+
'CAST',
|
|
1241
|
+
'COALESCE',
|
|
1242
|
+
'CEIL',
|
|
1243
|
+
'CEILING',
|
|
1244
|
+
'FLOOR',
|
|
1245
|
+
'ABS',
|
|
1246
|
+
'CONCAT',
|
|
1247
|
+
'NULLIF',
|
|
1248
|
+
'IFNULL',
|
|
1249
|
+
'GREATEST',
|
|
1250
|
+
'LEAST',
|
|
1251
|
+
'TRUNC',
|
|
1252
|
+
'TRUNCATE',
|
|
1253
|
+
'MOD',
|
|
1254
|
+
'POWER',
|
|
1255
|
+
'SQRT',
|
|
1256
|
+
'LN',
|
|
1257
|
+
'LOG',
|
|
1258
|
+
'EXP',
|
|
1259
|
+
'SUBSTR',
|
|
1260
|
+
'SUBSTRING',
|
|
1261
|
+
'TO_CHAR',
|
|
1262
|
+
'TO_NUMBER',
|
|
1263
|
+
'TO_DATE',
|
|
1264
|
+
]);
|
|
1265
|
+
const FUNCTION_CALL_WITH_OVER_PATTERN = /\b(?<fn>[A-Z_]{1,32})\s*\((?:[^()]|\([^()]*\)){0,200}\bOVER\s*\(/gi;
|
|
1266
|
+
function hasWindowInsideNonWindowFunctionCall(sql) {
|
|
1267
|
+
FUNCTION_CALL_WITH_OVER_PATTERN.lastIndex = 0;
|
|
1268
|
+
let match;
|
|
1269
|
+
while ((match = FUNCTION_CALL_WITH_OVER_PATTERN.exec(sql)) !== null) {
|
|
1270
|
+
const fn = match.groups?.fn?.toUpperCase();
|
|
1271
|
+
if (fn && NON_WINDOW_SCALAR_FUNCTIONS.has(fn)) {
|
|
1272
|
+
return true;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
return false;
|
|
1276
|
+
}
|
|
1277
|
+
const LATERAL_SUBQUERY_PATTERN = /\b(?:CROSS|INNER|LEFT|RIGHT)?\s*JOIN\s+LATERAL\b|\bLATERAL\s*\(/i;
|
|
1278
|
+
export function detectUnsupportedEnginePattern(sql) {
|
|
1279
|
+
if (NESTED_AGGREGATE_PATTERN.test(sql) && OVER_CLAUSE_PATTERN.test(sql)) {
|
|
1280
|
+
return {
|
|
1281
|
+
kind: 'NESTED_AGGREGATE_IN_WINDOW',
|
|
1282
|
+
hint: "The engine's SQL→Pure translator cannot combine an aggregate function with a window function in the same SELECT (e.g. SUM(COUNT(*)) OVER ()). Move the aggregation into a CTE first, then apply the window over the CTE.",
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
if (AGGREGATE_IN_WINDOW_ARGS_PATTERN.test(sql)) {
|
|
1286
|
+
return {
|
|
1287
|
+
kind: 'AGGREGATE_IN_WINDOW_ARGS',
|
|
1288
|
+
hint: "The engine's SQL→Pure translator cannot evaluate an aggregate function inside an OVER clause's PARTITION BY or ORDER BY at the same level as GROUP BY (e.g. ROW_NUMBER() OVER (ORDER BY COUNT(*) DESC)). Materialize the aggregate into a CTE column first, then reference that column from the window — for example: SELECT col, cnt, ROW_NUMBER() OVER (ORDER BY cnt DESC) AS rn FROM (SELECT col, COUNT(*) AS cnt FROM p('…') GROUP BY col) agg.",
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
if (hasWindowInsideNonWindowFunctionCall(sql)) {
|
|
1292
|
+
return {
|
|
1293
|
+
kind: 'WINDOW_INSIDE_FUNCTION_CALL',
|
|
1294
|
+
hint: "The engine's SQL→Pure translator cannot evaluate a window function inside another function call (e.g. ROUND(SUM(x) OVER (), 2)). Materialize the window expression into its own column in a subquery or CTE, then apply the wrapping function in the outer SELECT — for example: SELECT col, ROUND(cnt * 100.0 / total, 2) FROM (SELECT col, cnt, SUM(cnt) OVER () AS total FROM (SELECT col, COUNT(*) AS cnt FROM p('…') GROUP BY col) agg) windowed.",
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
if (LATERAL_SUBQUERY_PATTERN.test(sql)) {
|
|
1298
|
+
return {
|
|
1299
|
+
kind: 'LATERAL_SUBQUERY',
|
|
1300
|
+
hint: "The engine's SQL parser does not support LATERAL subqueries (CROSS JOIN LATERAL (...)). Rewrite using ROW_NUMBER() OVER (PARTITION BY ...) in a CTE and filter on the rank, or use a correlated subquery.",
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
return undefined;
|
|
1304
|
+
}
|
|
900
1305
|
/**
|
|
901
1306
|
* Appends a safety LIMIT to queries that lack one, preventing unbounded
|
|
902
1307
|
* result sets on large services. Skips aggregation queries since those
|
|
@@ -908,8 +1313,23 @@ export function ensureSafeLimit(sql, limit = DEFAULT_SAFETY_LIMIT) {
|
|
|
908
1313
|
}
|
|
909
1314
|
return `${sql.trimEnd()}\nLIMIT ${limit}`;
|
|
910
1315
|
}
|
|
1316
|
+
function prepareSafeSql(sql, services) {
|
|
1317
|
+
const safeSql = ensureSafeLimit(ensureDateParameters(sanitizeLiteralColumns(sanitizeJoinSameKeyColumns(sanitizeJoinOrderBy(sql), services)), services));
|
|
1318
|
+
const unsupported = detectUnsupportedEnginePattern(safeSql);
|
|
1319
|
+
if (unsupported) {
|
|
1320
|
+
throw new LegendAIUnsupportedEngineShapeError(unsupported.hint);
|
|
1321
|
+
}
|
|
1322
|
+
return safeSql;
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* The single execution chokepoint for SQL in this module. Every code
|
|
1326
|
+
* path that needs to run SQL — primary orchestrator, retry-with-fix,
|
|
1327
|
+
* exported `executeSqlAndReport`, internal helpers — calls through
|
|
1328
|
+
* here. No other code in this file may call `plugin.executeLakehouseSql`
|
|
1329
|
+
* or `plugin.executeSql` directly.
|
|
1330
|
+
*/
|
|
911
1331
|
async function executeSqlForServices(sql, services, dataProductCoordinates, plugin, config) {
|
|
912
|
-
const safeSql =
|
|
1332
|
+
const safeSql = prepareSafeSql(sql, services);
|
|
913
1333
|
const isAccessPoint = services.some((s) => s.sourceType === TDSServiceSourceType.ACCESS_POINT);
|
|
914
1334
|
if (isAccessPoint && dataProductCoordinates) {
|
|
915
1335
|
return plugin.executeLakehouseSql(safeSql, dataProductCoordinates, config);
|
|
@@ -1106,61 +1526,162 @@ async function reportQueryResults(report, metadata, context, startTime, hasOrche
|
|
|
1106
1526
|
}));
|
|
1107
1527
|
}
|
|
1108
1528
|
}
|
|
1529
|
+
export function supplementMissingCoverage(question, selected, allServices, maxTotal = 4) {
|
|
1530
|
+
if (selected.length >= maxTotal || allServices.length <= selected.length) {
|
|
1531
|
+
return selected;
|
|
1532
|
+
}
|
|
1533
|
+
const questionTokens = tokenizeText(question);
|
|
1534
|
+
if (questionTokens.length === 0) {
|
|
1535
|
+
return selected;
|
|
1536
|
+
}
|
|
1537
|
+
const coveredTokens = new Set();
|
|
1538
|
+
for (const svc of selected) {
|
|
1539
|
+
for (const col of svc.columns) {
|
|
1540
|
+
for (const token of splitIdentifierTokens(col.name)) {
|
|
1541
|
+
coveredTokens.add(token);
|
|
1542
|
+
}
|
|
1543
|
+
coveredTokens.add(col.name.toLowerCase());
|
|
1544
|
+
}
|
|
1545
|
+
for (const token of splitIdentifierTokens(svc.title)) {
|
|
1546
|
+
coveredTokens.add(token);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
const coveredArray = Array.from(coveredTokens);
|
|
1550
|
+
const uncovered = questionTokens.filter((t) => !coveredTokens.has(t) &&
|
|
1551
|
+
!(t.length >= 4 && coveredArray.some((ct) => isFuzzyMatch(t, ct))));
|
|
1552
|
+
if (uncovered.length === 0) {
|
|
1553
|
+
return selected;
|
|
1554
|
+
}
|
|
1555
|
+
const selectedSet = new Set(selected.map((s) => s.pattern));
|
|
1556
|
+
const supplementCandidates = allServices
|
|
1557
|
+
.filter((svc) => !selectedSet.has(svc.pattern))
|
|
1558
|
+
.map((svc) => {
|
|
1559
|
+
const svcTokens = new Set();
|
|
1560
|
+
for (const col of svc.columns) {
|
|
1561
|
+
svcTokens.add(col.name.toLowerCase());
|
|
1562
|
+
for (const token of splitIdentifierTokens(col.name)) {
|
|
1563
|
+
svcTokens.add(token);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
for (const token of splitIdentifierTokens(svc.title)) {
|
|
1567
|
+
svcTokens.add(token);
|
|
1568
|
+
}
|
|
1569
|
+
const coverCount = uncovered.filter((t) => svcTokens.has(t)).length;
|
|
1570
|
+
const isGeneric = GENERIC_TABLE_PATTERNS.test(svc.title) ? 1 : 0;
|
|
1571
|
+
return { svc, coverCount, isGeneric };
|
|
1572
|
+
})
|
|
1573
|
+
.filter((c) => c.coverCount > 0)
|
|
1574
|
+
.sort((a, b) => b.coverCount - a.coverCount || a.isGeneric - b.isGeneric);
|
|
1575
|
+
const result = [...selected];
|
|
1576
|
+
for (const candidate of supplementCandidates) {
|
|
1577
|
+
if (result.length >= maxTotal) {
|
|
1578
|
+
break;
|
|
1579
|
+
}
|
|
1580
|
+
result.push(candidate.svc);
|
|
1581
|
+
}
|
|
1582
|
+
return result;
|
|
1583
|
+
}
|
|
1584
|
+
// ─── Question Normalization ──────────────────────────────────────────────────
|
|
1585
|
+
/** Delegates to the plugin's LLM to fix natural-language typos in the user's question before AP selection; falls back to the original on failure. */
|
|
1586
|
+
export async function normalizeQuestion(question, plugin, config) {
|
|
1587
|
+
try {
|
|
1588
|
+
return await plugin.normalizeQuestion(question, config);
|
|
1589
|
+
}
|
|
1590
|
+
catch (error) {
|
|
1591
|
+
assertErrorThrown(error);
|
|
1592
|
+
return question;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
// ─── Multi-Turn AP Bias ──────────────────────────────────────────────────────
|
|
1109
1596
|
/**
|
|
1110
|
-
*
|
|
1111
|
-
*
|
|
1112
|
-
*
|
|
1113
|
-
* descending relevance score.
|
|
1597
|
+
* Extracts AP patterns that were successfully used in previous conversation
|
|
1598
|
+
* turns. When the user asks a follow-up question, these APs should be
|
|
1599
|
+
* preferred since they're likely still relevant.
|
|
1114
1600
|
*/
|
|
1115
|
-
|
|
1116
|
-
const
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
svc.description ?? '',
|
|
1127
|
-
svc.pattern,
|
|
1128
|
-
...svc.columns.slice(0, 30).map((c) => c.name),
|
|
1129
|
-
...svc.parameters,
|
|
1130
|
-
...(svc.preFilters ?? []).flatMap((pf) => {
|
|
1131
|
-
const parts = [pf.property, pf.operator];
|
|
1132
|
-
if (pf.value !== undefined) {
|
|
1133
|
-
parts.push(String(pf.value));
|
|
1601
|
+
function extractPreviousTurnAPPatterns(history) {
|
|
1602
|
+
const patterns = new Set();
|
|
1603
|
+
for (const turn of history) {
|
|
1604
|
+
const pCallPattern = /p\(\s*'(?<pId>[^']+)'\s*\)/g;
|
|
1605
|
+
let pCall;
|
|
1606
|
+
while ((pCall = pCallPattern.exec(turn.sql)) !== null) {
|
|
1607
|
+
const pattern = pCall.groups?.pId;
|
|
1608
|
+
if (pattern) {
|
|
1609
|
+
const lastDot = pattern.lastIndexOf('.');
|
|
1610
|
+
if (lastDot >= 0) {
|
|
1611
|
+
patterns.add(pattern.slice(lastDot + 1).toLowerCase());
|
|
1134
1612
|
}
|
|
1135
|
-
|
|
1136
|
-
}),
|
|
1137
|
-
]
|
|
1138
|
-
.join(' ')
|
|
1139
|
-
.toLowerCase();
|
|
1140
|
-
let score = 0;
|
|
1141
|
-
for (const token of queryTokens) {
|
|
1142
|
-
if (haystack.includes(token)) {
|
|
1143
|
-
score += 1;
|
|
1613
|
+
patterns.add(pattern.toLowerCase());
|
|
1144
1614
|
}
|
|
1145
1615
|
}
|
|
1146
|
-
|
|
1616
|
+
}
|
|
1617
|
+
return patterns;
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1620
|
+
* Applies a scoring boost to services whose patterns appeared in previous
|
|
1621
|
+
* conversation turns, biasing the pre-filter toward continuity when the
|
|
1622
|
+
* user asks follow-up questions about the same data.
|
|
1623
|
+
*/
|
|
1624
|
+
export function applyMultiTurnBias(services, history) {
|
|
1625
|
+
if (history.length === 0) {
|
|
1626
|
+
return services;
|
|
1627
|
+
}
|
|
1628
|
+
const previousAPs = extractPreviousTurnAPPatterns(history);
|
|
1629
|
+
if (previousAPs.size === 0) {
|
|
1630
|
+
return services;
|
|
1631
|
+
}
|
|
1632
|
+
// Sort so that previously-used APs come first, preserving relative order
|
|
1633
|
+
const biased = [...services].sort((a, b) => {
|
|
1634
|
+
const aUsed = previousAPs.has(a.pattern.replace(/^\//, '').toLowerCase()) ||
|
|
1635
|
+
previousAPs.has(a.title.toLowerCase())
|
|
1636
|
+
? 1
|
|
1637
|
+
: 0;
|
|
1638
|
+
const bUsed = previousAPs.has(b.pattern.replace(/^\//, '').toLowerCase()) ||
|
|
1639
|
+
previousAPs.has(b.title.toLowerCase())
|
|
1640
|
+
? 1
|
|
1641
|
+
: 0;
|
|
1642
|
+
return bUsed - aUsed;
|
|
1147
1643
|
});
|
|
1148
|
-
|
|
1149
|
-
return scored.slice(0, limit).map((s) => s.svc);
|
|
1644
|
+
return biased;
|
|
1150
1645
|
}
|
|
1151
1646
|
async function selectBestServices(question, services, context) {
|
|
1152
|
-
const { plugin, config, setMessages } = context;
|
|
1647
|
+
const { plugin, config, setMessages, history } = context;
|
|
1153
1648
|
if (services.length <= 1) {
|
|
1154
1649
|
return services;
|
|
1155
1650
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1651
|
+
// ── Step 0: Normalize the question (expand abbreviations, fix typos) ──
|
|
1652
|
+
addThinkingStep(setMessages, 'Normalizing question...');
|
|
1653
|
+
const normalizedQuestion = await normalizeQuestion(question, plugin, config);
|
|
1654
|
+
if (normalizedQuestion !== question) {
|
|
1655
|
+
addThinkingStep(setMessages, `Normalized: "${normalizedQuestion}"`);
|
|
1656
|
+
}
|
|
1657
|
+
// ── Step 0b: Apply multi-turn bias if this is a follow-up ──
|
|
1658
|
+
let inputServices = services;
|
|
1659
|
+
if (history.length > 0) {
|
|
1660
|
+
inputServices = applyMultiTurnBias(services, history);
|
|
1661
|
+
}
|
|
1662
|
+
let candidates = inputServices;
|
|
1663
|
+
if (inputServices.length > MAX_SERVICES_FOR_LLM_SELECTION) {
|
|
1664
|
+
candidates = preFilterServicesByRelevance(normalizedQuestion, inputServices, MAX_SERVICES_FOR_LLM_SELECTION);
|
|
1665
|
+
const topNames = candidates
|
|
1666
|
+
.slice(0, 5)
|
|
1667
|
+
.map((s) => `${s.title} (${s.columns.length} cols)`)
|
|
1668
|
+
.join(', ');
|
|
1669
|
+
addThinkingStep(setMessages, `Pre-filtered ${services.length} services to ${candidates.length} candidates. Top: ${topNames}`);
|
|
1160
1670
|
}
|
|
1161
1671
|
try {
|
|
1162
1672
|
addThinkingStep(setMessages, 'Selecting best service for your query...');
|
|
1163
|
-
|
|
1673
|
+
const selected = await plugin.selectRelevantServices(normalizedQuestion, candidates, config);
|
|
1674
|
+
const selectedNames = selected.map((s) => s.title).join(', ');
|
|
1675
|
+
addThinkingStep(setMessages, `LLM selected: ${selectedNames}`);
|
|
1676
|
+
const validated = supplementMissingCoverage(normalizedQuestion, selected, candidates);
|
|
1677
|
+
if (validated.length > selected.length) {
|
|
1678
|
+
const supplemented = validated
|
|
1679
|
+
.slice(selected.length)
|
|
1680
|
+
.map((s) => s.title)
|
|
1681
|
+
.join(', ');
|
|
1682
|
+
addThinkingStep(setMessages, `Supplemented with ${supplemented} for column coverage`);
|
|
1683
|
+
}
|
|
1684
|
+
return validated;
|
|
1164
1685
|
}
|
|
1165
1686
|
catch (selectionError) {
|
|
1166
1687
|
assertErrorThrown(selectionError);
|
|
@@ -1197,64 +1718,131 @@ async function tryRecoverZeroRows(currentSql, sqlResult, question, selectedServi
|
|
|
1197
1718
|
}
|
|
1198
1719
|
return { sql: recoveredSql, result: recoveredResult };
|
|
1199
1720
|
}
|
|
1721
|
+
async function resolveNestedPCalls(sql, question, selectedAPs, context, modelContextEnrichment) {
|
|
1722
|
+
if (!hasNestedPCalls(sql)) {
|
|
1723
|
+
return sql;
|
|
1724
|
+
}
|
|
1725
|
+
const { plugin, config, setMessages } = context;
|
|
1726
|
+
addThinkingStep(setMessages, 'Detected nested p() call — rewriting query...');
|
|
1727
|
+
const fixPrompt = plugin.buildAccessPointErrorCorrectionPrompt(sql, question, 'The SQL contains p() inside a subquery (IN clause, CROSS JOIN, or scalar subquery). p() can ONLY appear directly after FROM or JOIN.', selectedAPs, context.history, modelContextEnrichment);
|
|
1728
|
+
try {
|
|
1729
|
+
const fixResponse = await plugin.callLLM(fixPrompt, config);
|
|
1730
|
+
const fixResult = plugin.extractJudgeResult(fixResponse);
|
|
1731
|
+
const corrected = fixResult.correctedSql;
|
|
1732
|
+
if (corrected && !hasNestedPCalls(corrected)) {
|
|
1733
|
+
return corrected;
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
catch (fixError) {
|
|
1737
|
+
assertErrorThrown(fixError);
|
|
1738
|
+
addThinkingStep(setMessages, `Could not rewrite query: ${fixError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}. Proceeding with original SQL.`);
|
|
1739
|
+
}
|
|
1740
|
+
return sql;
|
|
1741
|
+
}
|
|
1742
|
+
async function retryAccessPointSqlFix(options, context, metadata) {
|
|
1743
|
+
const { failedSql, question, errMsg, selectedAPs, dataProductCoordinates, modelContextEnrichment, startTime, } = options;
|
|
1744
|
+
const { plugin, config, setMessages } = context;
|
|
1745
|
+
addThinkingStep(setMessages, 'SQL error detected — asking LLM to fix the query on the same access points...');
|
|
1746
|
+
const fixPrompt = plugin.buildAccessPointErrorCorrectionPrompt(failedSql, question, errMsg.slice(0, MAX_ERROR_MESSAGE_LENGTH), selectedAPs, context.history, modelContextEnrichment);
|
|
1747
|
+
try {
|
|
1748
|
+
const fixResponse = await plugin.callLLM(fixPrompt, config);
|
|
1749
|
+
const fixResult = plugin.extractJudgeResult(fixResponse);
|
|
1750
|
+
const correctedSql = fixResult.correctedSql;
|
|
1751
|
+
if (correctedSql) {
|
|
1752
|
+
addThinkingStep(setMessages, 'Re-executing corrected SQL...');
|
|
1753
|
+
updateLastAssistant(setMessages, () => ({
|
|
1754
|
+
sql: correctedSql,
|
|
1755
|
+
isExecuting: true,
|
|
1756
|
+
}));
|
|
1757
|
+
const retryResult = await executeSqlForServices(correctedSql, selectedAPs, dataProductCoordinates, plugin, config);
|
|
1758
|
+
await reportQueryResults({
|
|
1759
|
+
currentSql: correctedSql,
|
|
1760
|
+
sqlResult: retryResult,
|
|
1761
|
+
question,
|
|
1762
|
+
services: selectedAPs,
|
|
1763
|
+
}, metadata, context, startTime, false);
|
|
1764
|
+
return true;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
catch (retryError) {
|
|
1768
|
+
assertErrorThrown(retryError);
|
|
1769
|
+
addThinkingStep(setMessages, `SQL correction also failed: ${retryError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
1770
|
+
}
|
|
1771
|
+
return false;
|
|
1772
|
+
}
|
|
1200
1773
|
/**
|
|
1201
1774
|
* Dedicated query flow for data product access points.
|
|
1202
1775
|
* Unlike processDataQuery this:
|
|
1203
1776
|
* - Uses AP-specific generator/judge prompts (p() syntax, no coordinates/params)
|
|
1204
1777
|
* - Skips parameter detection (APs have no parameters)
|
|
1205
1778
|
*/
|
|
1206
|
-
async function processAccessPointQuery(question, accessPoints, metadata, context, startTime, dataProductCoordinates) {
|
|
1779
|
+
async function processAccessPointQuery(question, accessPoints, metadata, context, startTime, dataProductCoordinates, modelContextEnrichment) {
|
|
1207
1780
|
const { config, plugin, setMessages } = context;
|
|
1208
|
-
const hasOrchestratorFallback = Boolean(config.orchestratorUrl);
|
|
1209
1781
|
addThinkingStep(setMessages, 'Found relevant access points to query');
|
|
1210
1782
|
const selectedAPs = await selectBestServices(question, accessPoints, context);
|
|
1211
|
-
const judgedSql = await generateAndJudgeAccessPointSql(question, selectedAPs, context, startTime);
|
|
1783
|
+
const judgedSql = await generateAndJudgeAccessPointSql(question, selectedAPs, context, startTime, modelContextEnrichment);
|
|
1212
1784
|
if (!judgedSql) {
|
|
1213
1785
|
addThinkingStep(setMessages, 'SQL generation could not produce a valid query.');
|
|
1214
|
-
handleSqlGenerationFailure(setMessages, startTime,
|
|
1786
|
+
handleSqlGenerationFailure(setMessages, startTime, false, SQL_GENERATION_FAILURE_WITH_ORCHESTRATOR, SQL_GENERATION_FAILURE_NO_ORCHESTRATOR, LegendAIErrorType.GENERATION, buildFallbackSuggestions(selectedAPs));
|
|
1215
1787
|
return;
|
|
1216
1788
|
}
|
|
1217
1789
|
const sqlGenTimeValue = elapsedSeconds(startTime, 2);
|
|
1218
1790
|
completeThinkingSteps(setMessages);
|
|
1791
|
+
const finalSql = await resolveNestedPCalls(judgedSql, question, selectedAPs, context, modelContextEnrichment);
|
|
1219
1792
|
updateLastAssistant(setMessages, () => ({
|
|
1220
|
-
sql:
|
|
1793
|
+
sql: finalSql,
|
|
1221
1794
|
sqlGenTime: sqlGenTimeValue,
|
|
1222
1795
|
isExecuting: true,
|
|
1223
1796
|
}));
|
|
1224
1797
|
const execStartTime = Date.now();
|
|
1225
1798
|
try {
|
|
1226
|
-
const
|
|
1227
|
-
const rawResult = await plugin.executeLakehouseSql(safeSql, dataProductCoordinates, config);
|
|
1799
|
+
const rawResult = await executeSqlForServices(finalSql, selectedAPs, dataProductCoordinates, plugin, config);
|
|
1228
1800
|
await reportQueryResults({
|
|
1229
|
-
currentSql:
|
|
1801
|
+
currentSql: finalSql,
|
|
1230
1802
|
sqlResult: rawResult,
|
|
1231
1803
|
question,
|
|
1232
1804
|
services: selectedAPs,
|
|
1233
|
-
}, metadata, context, startTime,
|
|
1805
|
+
}, metadata, context, startTime, false);
|
|
1234
1806
|
}
|
|
1235
1807
|
catch (executeError) {
|
|
1236
1808
|
assertErrorThrown(executeError);
|
|
1237
1809
|
const execErrorType = classifyError(executeError);
|
|
1238
|
-
|
|
1239
|
-
|
|
1810
|
+
const errMsg = executeError.message;
|
|
1811
|
+
const errorCategory = categorizeExecutionError(errMsg, executeError);
|
|
1812
|
+
addThinkingStep(setMessages, `Execution failed: ${errMsg.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
1813
|
+
if (errorCategory === ExecutionErrorCategory.SQL_FIXABLE) {
|
|
1814
|
+
const retried = await retryAccessPointSqlFix({
|
|
1815
|
+
failedSql: finalSql,
|
|
1816
|
+
question,
|
|
1817
|
+
errMsg,
|
|
1818
|
+
selectedAPs,
|
|
1819
|
+
dataProductCoordinates,
|
|
1820
|
+
...(modelContextEnrichment === undefined
|
|
1821
|
+
? {}
|
|
1822
|
+
: { modelContextEnrichment }),
|
|
1823
|
+
startTime,
|
|
1824
|
+
}, context, metadata);
|
|
1825
|
+
if (retried) {
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
finishWithThinkingError(setMessages, buildExecutionErrorMessage(errMsg, selectedAPs), startTime, execErrorType === LegendAIErrorType.GENERAL
|
|
1240
1830
|
? LegendAIErrorType.EXECUTION
|
|
1241
1831
|
: execErrorType);
|
|
1832
|
+
const queriedGroups = [
|
|
1833
|
+
...new Set(selectedAPs
|
|
1834
|
+
.map((ap) => ap.accessPointGroupTitle)
|
|
1835
|
+
.filter((t) => t !== undefined)),
|
|
1836
|
+
];
|
|
1242
1837
|
updateLastAssistant(setMessages, () => ({
|
|
1243
1838
|
execTime: elapsedSeconds(execStartTime, 2),
|
|
1244
1839
|
isExecuting: false,
|
|
1245
1840
|
suggestedQueries: buildFallbackSuggestions(selectedAPs),
|
|
1246
|
-
|
|
1247
|
-
? {
|
|
1248
|
-
fallbackAction: {
|
|
1249
|
-
label: ORCHESTRATOR_FALLBACK_LABEL,
|
|
1250
|
-
actionId: LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID,
|
|
1251
|
-
},
|
|
1252
|
-
}
|
|
1253
|
-
: {}),
|
|
1841
|
+
queriedAccessPointGroups: queriedGroups,
|
|
1254
1842
|
}));
|
|
1255
1843
|
}
|
|
1256
1844
|
}
|
|
1257
|
-
async function processDataQuery(question, services, coordinates, metadata, context, startTime, orchestratorOptions) {
|
|
1845
|
+
async function processDataQuery(question, services, coordinates, metadata, context, startTime, orchestratorOptions, modelContextEnrichment) {
|
|
1258
1846
|
const { config, setMessages } = context;
|
|
1259
1847
|
const dataProductCoordinates = orchestratorOptions?.dataProductCoordinates;
|
|
1260
1848
|
const hasOrchestratorFallback = Boolean(config.orchestratorUrl && dataProductCoordinates);
|
|
@@ -1264,7 +1852,7 @@ async function processDataQuery(question, services, coordinates, metadata, conte
|
|
|
1264
1852
|
}
|
|
1265
1853
|
addThinkingStep(setMessages, 'Found relevant services to query');
|
|
1266
1854
|
const selectedServices = await selectBestServices(question, services, context);
|
|
1267
|
-
const judgedSql = await generateAndJudgeSql(question, selectedServices, coordinates, context, startTime, metadata);
|
|
1855
|
+
const judgedSql = await generateAndJudgeSql(question, selectedServices, coordinates, context, startTime, metadata, modelContextEnrichment);
|
|
1268
1856
|
if (!judgedSql) {
|
|
1269
1857
|
addThinkingStep(setMessages, 'SQL generation could not produce a valid query.');
|
|
1270
1858
|
handleSqlGenerationFailure(setMessages, startTime, hasOrchestratorFallback, SQL_GENERATION_FAILURE_WITH_ORCHESTRATOR, SQL_GENERATION_FAILURE_NO_ORCHESTRATOR, LegendAIErrorType.GENERATION, buildFallbackSuggestions(selectedServices));
|
|
@@ -1338,45 +1926,24 @@ function splitServicesByType(services) {
|
|
|
1338
1926
|
accessPoints: services.filter((s) => s.sourceType === TDSServiceSourceType.ACCESS_POINT),
|
|
1339
1927
|
};
|
|
1340
1928
|
}
|
|
1341
|
-
async function routeToAccessPointOrData(question, services, coordinates, metadata, context, startTime, orchestratorOpts) {
|
|
1929
|
+
async function routeToAccessPointOrData(question, services, coordinates, metadata, context, startTime, orchestratorOpts, modelContextEnrichment) {
|
|
1342
1930
|
const dataProductCoordinates = orchestratorOpts?.dataProductCoordinates;
|
|
1343
1931
|
const { tdsServices, accessPoints } = splitServicesByType(services);
|
|
1344
1932
|
if (tdsServices.length === 0 &&
|
|
1345
1933
|
accessPoints.length > 0 &&
|
|
1346
1934
|
dataProductCoordinates) {
|
|
1347
|
-
await processAccessPointQuery(question, accessPoints, metadata, context, startTime, dataProductCoordinates);
|
|
1935
|
+
await processAccessPointQuery(question, accessPoints, metadata, context, startTime, dataProductCoordinates, modelContextEnrichment);
|
|
1348
1936
|
}
|
|
1349
1937
|
else {
|
|
1350
|
-
await processDataQuery(question, tdsServices, coordinates, metadata, context, startTime, orchestratorOpts);
|
|
1938
|
+
await processDataQuery(question, tdsServices, coordinates, metadata, context, startTime, orchestratorOpts, modelContextEnrichment);
|
|
1351
1939
|
}
|
|
1352
1940
|
}
|
|
1353
|
-
async function
|
|
1354
|
-
const { setMessages } = context;
|
|
1355
|
-
addThinkingStep(setMessages, 'Intent is ambiguous, providing metadata context and querying data...');
|
|
1356
|
-
let metadataOverview;
|
|
1357
|
-
try {
|
|
1358
|
-
addThinkingStep(setMessages, 'Building metadata context...');
|
|
1359
|
-
metadataOverview = await buildMetadataOverview(question, metadata, context);
|
|
1360
|
-
}
|
|
1361
|
-
catch (metadataError) {
|
|
1362
|
-
assertErrorThrown(metadataError);
|
|
1363
|
-
addThinkingStep(setMessages, `Metadata context failed: ${metadataError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
|
|
1364
|
-
}
|
|
1365
|
-
try {
|
|
1366
|
-
await routeToAccessPointOrData(question, services, coordinates, metadata, context, startTime, orchestratorOpts);
|
|
1367
|
-
if (metadataOverview) {
|
|
1368
|
-
attachMetadataOverview(setMessages, metadataOverview);
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
catch (queryError) {
|
|
1372
|
-
assertErrorThrown(queryError);
|
|
1373
|
-
addThinkingStep(setMessages, 'Query failed, answering from product metadata...');
|
|
1374
|
-
await handleMetadataQuestion(question, metadata, context, startTime, true);
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
export async function processQuestion(question, services, coordinates, metadata, context, dataProductCoordinates, pureExecutionContext) {
|
|
1941
|
+
export async function processQuestion(question, services, coordinates, metadata, context, dataProductCoordinates, pureExecutionContext, modelContext) {
|
|
1378
1942
|
const { config, plugin, setMessages } = context;
|
|
1379
1943
|
const startTime = Date.now();
|
|
1944
|
+
const modelContextEnrichment = modelContext
|
|
1945
|
+
? buildModelContextEnrichmentText(modelContext, services)
|
|
1946
|
+
: undefined;
|
|
1380
1947
|
try {
|
|
1381
1948
|
addThinkingStep(setMessages, 'Analyzing your question...');
|
|
1382
1949
|
const orchestratorOpts = dataProductCoordinates
|
|
@@ -1388,7 +1955,7 @@ export async function processQuestion(question, services, coordinates, metadata,
|
|
|
1388
1955
|
}
|
|
1389
1956
|
: undefined;
|
|
1390
1957
|
if (services.length === 0) {
|
|
1391
|
-
await handleMetadataQuestion(question, metadata, context, startTime, false);
|
|
1958
|
+
await handleMetadataQuestion(question, metadata, context, startTime, false, services, modelContextEnrichment);
|
|
1392
1959
|
if (config.orchestratorUrl && dataProductCoordinates) {
|
|
1393
1960
|
updateLastAssistant(setMessages, () => ({
|
|
1394
1961
|
fallbackAction: {
|
|
@@ -1399,29 +1966,19 @@ export async function processQuestion(question, services, coordinates, metadata,
|
|
|
1399
1966
|
}
|
|
1400
1967
|
return;
|
|
1401
1968
|
}
|
|
1402
|
-
const fastIntent = classifyQuestionIntentFast(question, true);
|
|
1403
|
-
if (fastIntent.intent === LegendAIQuestionIntent.METADATA &&
|
|
1404
|
-
!fastIntent.ambiguous) {
|
|
1405
|
-
await handleMetadataQuestion(question, metadata, context, startTime, true);
|
|
1406
|
-
return;
|
|
1407
|
-
}
|
|
1408
|
-
if (fastIntent.ambiguous) {
|
|
1409
|
-
await handleAmbiguousIntent(question, services, coordinates, metadata, context, startTime, orchestratorOpts);
|
|
1410
|
-
return;
|
|
1411
|
-
}
|
|
1412
1969
|
const serviceNames = services.map((s) => s.title);
|
|
1413
1970
|
const intent = await plugin.classifyQuestionIntent(question, true, config, serviceNames);
|
|
1414
1971
|
if (intent === LegendAIQuestionIntent.METADATA) {
|
|
1415
|
-
await handleMetadataQuestion(question, metadata, context, startTime, true);
|
|
1972
|
+
await handleMetadataQuestion(question, metadata, context, startTime, true, services, modelContextEnrichment);
|
|
1416
1973
|
return;
|
|
1417
1974
|
}
|
|
1418
1975
|
try {
|
|
1419
|
-
await routeToAccessPointOrData(question, services, coordinates, metadata, context, startTime, orchestratorOpts);
|
|
1976
|
+
await routeToAccessPointOrData(question, services, coordinates, metadata, context, startTime, orchestratorOpts, modelContextEnrichment);
|
|
1420
1977
|
}
|
|
1421
1978
|
catch (sqlError) {
|
|
1422
1979
|
assertErrorThrown(sqlError);
|
|
1423
|
-
addThinkingStep(setMessages, '
|
|
1424
|
-
await handleMetadataQuestion(question, metadata, context, startTime, true);
|
|
1980
|
+
addThinkingStep(setMessages, 'Query failed, answering from product metadata...');
|
|
1981
|
+
await handleMetadataQuestion(question, metadata, context, startTime, true, services, modelContextEnrichment);
|
|
1425
1982
|
appendFallbackSuggestions(setMessages, services);
|
|
1426
1983
|
}
|
|
1427
1984
|
}
|
|
@@ -1432,12 +1989,12 @@ export async function processQuestion(question, services, coordinates, metadata,
|
|
|
1432
1989
|
appendFallbackSuggestions(setMessages, services);
|
|
1433
1990
|
}
|
|
1434
1991
|
}
|
|
1435
|
-
async function routeDataQueryWithErrorHandling(question, services, coordinates, metadata, context, orchestratorOptions) {
|
|
1992
|
+
async function routeDataQueryWithErrorHandling(question, services, coordinates, metadata, context, orchestratorOptions, modelContextEnrichment) {
|
|
1436
1993
|
const { setMessages } = context;
|
|
1437
1994
|
const startTime = Date.now();
|
|
1438
1995
|
try {
|
|
1439
1996
|
addThinkingStep(setMessages, 'Preparing data query...');
|
|
1440
|
-
await routeToAccessPointOrData(question, services, coordinates, metadata, context, startTime, orchestratorOptions);
|
|
1997
|
+
await routeToAccessPointOrData(question, services, coordinates, metadata, context, startTime, orchestratorOptions, modelContextEnrichment);
|
|
1441
1998
|
}
|
|
1442
1999
|
catch (error) {
|
|
1443
2000
|
assertErrorThrown(error);
|
|
@@ -1445,13 +2002,16 @@ async function routeDataQueryWithErrorHandling(question, services, coordinates,
|
|
|
1445
2002
|
finishWithThinkingError(setMessages, error.message, startTime, classifyError(error));
|
|
1446
2003
|
}
|
|
1447
2004
|
}
|
|
1448
|
-
export async function processQuestionWithIntent(question, intent, services, coordinates, metadata, context, orchestratorOptions) {
|
|
2005
|
+
export async function processQuestionWithIntent(question, intent, services, coordinates, metadata, context, orchestratorOptions, modelContext) {
|
|
1449
2006
|
const { config, setMessages } = context;
|
|
1450
2007
|
const dataProductCoordinates = orchestratorOptions?.dataProductCoordinates;
|
|
2008
|
+
const modelContextEnrichment = modelContext
|
|
2009
|
+
? buildModelContextEnrichmentText(modelContext, services)
|
|
2010
|
+
: undefined;
|
|
1451
2011
|
if (intent === LegendAIQuestionIntent.METADATA) {
|
|
1452
2012
|
const startTime = Date.now();
|
|
1453
2013
|
try {
|
|
1454
|
-
await handleMetadataQuestion(question, metadata, context, startTime, services.length > 0);
|
|
2014
|
+
await handleMetadataQuestion(question, metadata, context, startTime, services.length > 0, services, modelContextEnrichment);
|
|
1455
2015
|
}
|
|
1456
2016
|
catch (error) {
|
|
1457
2017
|
assertErrorThrown(error);
|
|
@@ -1464,11 +2024,11 @@ export async function processQuestionWithIntent(question, intent, services, coor
|
|
|
1464
2024
|
config.orchestratorUrl &&
|
|
1465
2025
|
dataProductCoordinates) {
|
|
1466
2026
|
if (services.length > 0) {
|
|
1467
|
-
await routeDataQueryWithErrorHandling(question, services, coordinates, metadata, context, orchestratorOptions);
|
|
2027
|
+
await routeDataQueryWithErrorHandling(question, services, coordinates, metadata, context, orchestratorOptions, modelContextEnrichment);
|
|
1468
2028
|
return;
|
|
1469
2029
|
}
|
|
1470
2030
|
const startTime = Date.now();
|
|
1471
|
-
await handleMetadataQuestion(question, metadata, context, startTime, false);
|
|
2031
|
+
await handleMetadataQuestion(question, metadata, context, startTime, false, services, modelContextEnrichment);
|
|
1472
2032
|
updateLastAssistant(setMessages, () => ({
|
|
1473
2033
|
fallbackAction: {
|
|
1474
2034
|
label: ORCHESTRATOR_FALLBACK_LABEL,
|
|
@@ -1477,6 +2037,6 @@ export async function processQuestionWithIntent(question, intent, services, coor
|
|
|
1477
2037
|
}));
|
|
1478
2038
|
return;
|
|
1479
2039
|
}
|
|
1480
|
-
await routeDataQueryWithErrorHandling(question, services, coordinates, metadata, context, orchestratorOptions);
|
|
2040
|
+
await routeDataQueryWithErrorHandling(question, services, coordinates, metadata, context, orchestratorOptions, modelContextEnrichment);
|
|
1481
2041
|
}
|
|
1482
2042
|
//# sourceMappingURL=LegendAIChatProcessors.js.map
|