@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.
Files changed (75) hide show
  1. package/lib/index.css +2 -2
  2. package/lib/index.css.map +1 -1
  3. package/lib/legend-ai/LegendAIDocEnrichment.d.ts +34 -1
  4. package/lib/legend-ai/LegendAIDocEnrichment.d.ts.map +1 -1
  5. package/lib/legend-ai/LegendAIDocEnrichment.js +890 -1
  6. package/lib/legend-ai/LegendAIDocEnrichment.js.map +1 -1
  7. package/lib/legend-ai/LegendAIServiceRetrieval.d.ts +36 -0
  8. package/lib/legend-ai/LegendAIServiceRetrieval.d.ts.map +1 -0
  9. package/lib/legend-ai/LegendAIServiceRetrieval.js +179 -0
  10. package/lib/legend-ai/LegendAIServiceRetrieval.js.map +1 -0
  11. package/lib/legend-ai/LegendAITypes.d.ts +62 -2
  12. package/lib/legend-ai/LegendAITypes.d.ts.map +1 -1
  13. package/lib/legend-ai/LegendAITypes.js +56 -30
  14. package/lib/legend-ai/LegendAITypes.js.map +1 -1
  15. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts +29 -6
  16. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts.map +1 -1
  17. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js +9 -0
  18. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js.map +1 -1
  19. package/lib/legend-ai/__test-utils__/LegendAITestUtils.d.ts +5 -0
  20. package/lib/legend-ai/__test-utils__/LegendAITestUtils.d.ts.map +1 -1
  21. package/lib/legend-ai/__test-utils__/LegendAITestUtils.js +18 -2
  22. package/lib/legend-ai/__test-utils__/LegendAITestUtils.js.map +1 -1
  23. package/lib/legend-ai/components/LegendAIAnalysisPanel.d.ts.map +1 -1
  24. package/lib/legend-ai/components/LegendAIAnalysisPanel.js +27 -8
  25. package/lib/legend-ai/components/LegendAIAnalysisPanel.js.map +1 -1
  26. package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts +1 -0
  27. package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts.map +1 -1
  28. package/lib/legend-ai/components/LegendAIAnalysisUtils.js +144 -22
  29. package/lib/legend-ai/components/LegendAIAnalysisUtils.js.map +1 -1
  30. package/lib/legend-ai/components/LegendAICharts.d.ts +4 -0
  31. package/lib/legend-ai/components/LegendAICharts.d.ts.map +1 -1
  32. package/lib/legend-ai/components/LegendAICharts.js +60 -1
  33. package/lib/legend-ai/components/LegendAICharts.js.map +1 -1
  34. package/lib/legend-ai/components/LegendAIChat.d.ts.map +1 -1
  35. package/lib/legend-ai/components/LegendAIChat.js +46 -17
  36. package/lib/legend-ai/components/LegendAIChat.js.map +1 -1
  37. package/lib/legend-ai/components/LegendAIChatHelpers.d.ts +1 -1
  38. package/lib/legend-ai/components/LegendAIChatHelpers.d.ts.map +1 -1
  39. package/lib/legend-ai/components/LegendAIChatHelpers.js +220 -15
  40. package/lib/legend-ai/components/LegendAIChatHelpers.js.map +1 -1
  41. package/lib/legend-ai/components/LegendAIChatInput.d.ts.map +1 -1
  42. package/lib/legend-ai/components/LegendAIChatInput.js +1 -0
  43. package/lib/legend-ai/components/LegendAIChatInput.js.map +1 -1
  44. package/lib/legend-ai/components/LegendAIScopeSelector.d.ts.map +1 -1
  45. package/lib/legend-ai/components/LegendAIScopeSelector.js +5 -2
  46. package/lib/legend-ai/components/LegendAIScopeSelector.js.map +1 -1
  47. package/lib/legend-ai/index.d.ts +4 -3
  48. package/lib/legend-ai/index.d.ts.map +1 -1
  49. package/lib/legend-ai/index.js +4 -3
  50. package/lib/legend-ai/index.js.map +1 -1
  51. package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts +30 -13
  52. package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts.map +1 -1
  53. package/lib/legend-ai/stores/LegendAIChatProcessors.js +776 -216
  54. package/lib/legend-ai/stores/LegendAIChatProcessors.js.map +1 -1
  55. package/lib/legend-ai/stores/LegendAIChatState.d.ts +2 -2
  56. package/lib/legend-ai/stores/LegendAIChatState.d.ts.map +1 -1
  57. package/lib/legend-ai/stores/LegendAIChatState.js +26 -25
  58. package/lib/legend-ai/stores/LegendAIChatState.js.map +1 -1
  59. package/package.json +5 -5
  60. package/src/legend-ai/LegendAIDocEnrichment.ts +1198 -1
  61. package/src/legend-ai/LegendAIServiceRetrieval.ts +228 -0
  62. package/src/legend-ai/LegendAITypes.ts +138 -32
  63. package/src/legend-ai/LegendAI_LegendApplicationPlugin_Extension.ts +62 -0
  64. package/src/legend-ai/__test-utils__/LegendAITestUtils.ts +28 -0
  65. package/src/legend-ai/components/LegendAIAnalysisPanel.tsx +36 -17
  66. package/src/legend-ai/components/LegendAIAnalysisUtils.ts +178 -22
  67. package/src/legend-ai/components/LegendAICharts.tsx +160 -0
  68. package/src/legend-ai/components/LegendAIChat.tsx +107 -14
  69. package/src/legend-ai/components/LegendAIChatHelpers.ts +334 -20
  70. package/src/legend-ai/components/LegendAIChatInput.tsx +2 -1
  71. package/src/legend-ai/components/LegendAIScopeSelector.tsx +7 -2
  72. package/src/legend-ai/index.ts +22 -1
  73. package/src/legend-ai/stores/LegendAIChatProcessors.ts +1205 -321
  74. package/src/legend-ai/stores/LegendAIChatState.ts +80 -69
  75. 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 { classifyQuestionIntentFast, LegendAIQuestionIntent, LegendAIThinkingStepStatus, LegendAIMessageRole, LegendAIErrorType, LegendAIServiceError, TDSServiceSourceType, buildColumnDefsFromNames, LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID, getTodayISO, } from '../LegendAITypes.js';
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.gridData) {
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
- return services.slice(0, 3).map((svc) => `Show 10 records from ${svc.title}`);
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
- .toLowerCase()
287
- .split(/[^a-z0-9]+/)
288
- .filter((token) => token.length >= 4);
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 buildDeterministicResultSummary(question, _query, columns, rows) {
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 (row && typeof row === 'object' && !Array.isArray(row)) {
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
- const prompt = plugin.buildAccessPointGeneratorPrompt(question, accessPoints, history);
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 isAccessPoint = services.some((s) => s.sourceType === TDSServiceSourceType.ACCESS_POINT);
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(pureQuery, pureExecutionContext, dataProductCoordinates, config);
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 { columns: [], rows: [] };
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 handleEmptyOrchestratorResults(question, legendQuery, orchestratorOptions, metadata, resolvedEntities, context, startTime) {
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 = resolvedEntities.relatedEntities[0];
836
+ const alternateRoot = modelContext
837
+ ? findBestAlternateRoot(resolvedEntities.rootEntity, resolvedEntities.relatedEntities, modelContext)
838
+ : resolvedEntities.relatedEntities[0];
579
839
  if (alternateRoot) {
580
- addThinkingStep(setMessages, `No results with ${extractElementNameFromPath(resolvedEntities.rootEntity)}, retrying with ${extractElementNameFromPath(alternateRoot)}...`);
581
- try {
582
- const retryResponse = await plugin.generateQueryViaOrchestrator({
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
- export async function processQuestionViaOrchestrator(question, dataProductCoordinates, metadata, context, pureExecutionContext, preResolvedEntities) {
859
+ async function resolveOrchestrationEntities(question, dataProductCoordinates, context, pureExecutionContext, preResolvedEntities, modelContext) {
623
860
  const { config, plugin, setMessages } = context;
624
- const startTime = Date.now();
625
- try {
626
- let resolvedEntities;
627
- if (preResolvedEntities) {
628
- resolvedEntities = preResolvedEntities;
629
- addThinkingStep(setMessages, `Using pre-resolved root entity: ${extractElementNameFromPath(resolvedEntities.rootEntity)}`);
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
- else {
632
- addThinkingStep(setMessages, 'Resolving entities for your query...');
633
- resolvedEntities = await plugin.resolveEntitiesForQuery(question, dataProductCoordinates, config, pureExecutionContext);
634
- addThinkingStep(setMessages, `Found root entity: ${extractElementNameFromPath(resolvedEntities.rootEntity)}`);
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 orchestratorResponse = await plugin.generateQueryViaOrchestrator({
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
- const queryGenTime = elapsedSeconds(startTime, 2);
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: orchestratorResponse.legend_query,
652
- sqlGenTime: queryGenTime,
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(orchestratorResponse.legend_query, pureExecutionContext, dataProductCoordinates, config, plugin, setMessages, startTime);
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, orchestratorResponse.legend_query, execResult, metadata, context, startTime);
960
+ await analyzeOrchestratorResults(question, safeQuery, execResult, metadata, context, startTime);
670
961
  }
671
962
  else {
672
- await handleEmptyOrchestratorResults(question, orchestratorResponse.legend_query, { dataProductCoordinates, pureExecutionContext }, metadata, resolvedEntities, context, startTime);
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
- const orchErrorType = classifyError(error);
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 aliasRegex = /\b(?<tbl>[a-z]\w*)\s*\.\s*"(?<col>[^"]+)"\s+AS\s+(?:"(?<qAlias>[^"]+)"|(?<uAlias>\w+))/gi;
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
- const rewritten = afterOrderBy.replaceAll(ALIAS_DOT_COL_PATTERN, (...args) => {
747
- const groups = args[args.length - 1];
748
- const key = `${groups.tbl.toLowerCase()}.${groups.col.toLowerCase()}`;
749
- const alias = selectAliases.get(key);
750
- return alias ? `"${alias}"` : String(args[0]);
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
- const serviceCallPattern = /\bservice\s*\([^()]*\)/gi;
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 = ensureSafeLimit(ensureDateParameters(sanitizeLiteralColumns(sanitizeJoinOrderBy(sql)), services));
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
- * Scores each service against the user question by counting keyword
1111
- * overlap between the question tokens and the service title, description,
1112
- * column names, and parameter names. Returns services sorted by
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
- export function preFilterServicesByRelevance(question, services, limit) {
1116
- const queryTokens = question
1117
- .toLowerCase()
1118
- .split(/[\s,.:;!?'"()\-/]+/)
1119
- .filter((t) => t.length > 2);
1120
- if (queryTokens.length === 0) {
1121
- return services.slice(0, limit);
1122
- }
1123
- const scored = services.map((svc) => {
1124
- const haystack = [
1125
- svc.title,
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
- return parts;
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
- return { svc, score };
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
- scored.sort((a, b) => b.score - a.score);
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
- let candidates = services;
1157
- if (services.length > MAX_SERVICES_FOR_LLM_SELECTION) {
1158
- candidates = preFilterServicesByRelevance(question, services, MAX_SERVICES_FOR_LLM_SELECTION);
1159
- addThinkingStep(setMessages, `Pre-filtered ${services.length} services to ${candidates.length} candidates`);
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
- return await plugin.selectRelevantServices(question, candidates, config);
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, hasOrchestratorFallback, SQL_GENERATION_FAILURE_WITH_ORCHESTRATOR, SQL_GENERATION_FAILURE_NO_ORCHESTRATOR, LegendAIErrorType.GENERATION, buildFallbackSuggestions(selectedAPs));
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: judgedSql,
1793
+ sql: finalSql,
1221
1794
  sqlGenTime: sqlGenTimeValue,
1222
1795
  isExecuting: true,
1223
1796
  }));
1224
1797
  const execStartTime = Date.now();
1225
1798
  try {
1226
- const safeSql = ensureSafeLimit(judgedSql);
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: judgedSql,
1801
+ currentSql: finalSql,
1230
1802
  sqlResult: rawResult,
1231
1803
  question,
1232
1804
  services: selectedAPs,
1233
- }, metadata, context, startTime, hasOrchestratorFallback);
1805
+ }, metadata, context, startTime, false);
1234
1806
  }
1235
1807
  catch (executeError) {
1236
1808
  assertErrorThrown(executeError);
1237
1809
  const execErrorType = classifyError(executeError);
1238
- addThinkingStep(setMessages, `Execution failed: ${executeError.message.slice(0, MAX_THINKING_ERROR_PREVIEW_LENGTH)}`);
1239
- finishWithThinkingError(setMessages, buildExecutionErrorMessage(executeError.message, selectedAPs), startTime, execErrorType === LegendAIErrorType.GENERAL
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
- ...(hasOrchestratorFallback
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 handleAmbiguousIntent(question, services, coordinates, metadata, context, startTime, orchestratorOpts) {
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, 'SQL generation failed, answering from product metadata...');
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