@finos/legend-lego 2.0.211 → 2.0.213

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) 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 +12 -12
  4. package/lib/legend-ai/LegendAIDocEnrichment.d.ts.map +1 -1
  5. package/lib/legend-ai/LegendAIDocEnrichment.js +317 -86
  6. package/lib/legend-ai/LegendAIDocEnrichment.js.map +1 -1
  7. package/lib/legend-ai/LegendAIHostIntegration.d.ts +31 -0
  8. package/lib/legend-ai/LegendAIHostIntegration.d.ts.map +1 -0
  9. package/lib/legend-ai/LegendAIHostIntegration.js +50 -0
  10. package/lib/legend-ai/LegendAIHostIntegration.js.map +1 -0
  11. package/lib/legend-ai/LegendAIServiceRetrieval.d.ts.map +1 -1
  12. package/lib/legend-ai/LegendAIServiceRetrieval.js +34 -35
  13. package/lib/legend-ai/LegendAIServiceRetrieval.js.map +1 -1
  14. package/lib/legend-ai/LegendAITypes.d.ts +85 -7
  15. package/lib/legend-ai/LegendAITypes.d.ts.map +1 -1
  16. package/lib/legend-ai/LegendAITypes.js +99 -1
  17. package/lib/legend-ai/LegendAITypes.js.map +1 -1
  18. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts +36 -3
  19. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts.map +1 -1
  20. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js +29 -0
  21. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js.map +1 -1
  22. package/lib/legend-ai/__test-utils__/LegendAITestUtils.d.ts.map +1 -1
  23. package/lib/legend-ai/__test-utils__/LegendAITestUtils.js +7 -0
  24. package/lib/legend-ai/__test-utils__/LegendAITestUtils.js.map +1 -1
  25. package/lib/legend-ai/components/LegendAICharts.d.ts.map +1 -1
  26. package/lib/legend-ai/components/LegendAICharts.js +6 -10
  27. package/lib/legend-ai/components/LegendAICharts.js.map +1 -1
  28. package/lib/legend-ai/components/LegendAIChat.d.ts.map +1 -1
  29. package/lib/legend-ai/components/LegendAIChat.js +263 -41
  30. package/lib/legend-ai/components/LegendAIChat.js.map +1 -1
  31. package/lib/legend-ai/components/LegendAIChatInput.d.ts +1 -1
  32. package/lib/legend-ai/components/LegendAIChatInput.d.ts.map +1 -1
  33. package/lib/legend-ai/components/LegendAIChatInput.js +2 -1
  34. package/lib/legend-ai/components/LegendAIChatInput.js.map +1 -1
  35. package/lib/legend-ai/index.d.ts +5 -2
  36. package/lib/legend-ai/index.d.ts.map +1 -1
  37. package/lib/legend-ai/index.js +5 -2
  38. package/lib/legend-ai/index.js.map +1 -1
  39. package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts +9 -3
  40. package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts.map +1 -1
  41. package/lib/legend-ai/stores/LegendAIChatProcessors.js +794 -220
  42. package/lib/legend-ai/stores/LegendAIChatProcessors.js.map +1 -1
  43. package/lib/legend-ai/stores/LegendAIChatState.d.ts.map +1 -1
  44. package/lib/legend-ai/stores/LegendAIChatState.js +47 -21
  45. package/lib/legend-ai/stores/LegendAIChatState.js.map +1 -1
  46. package/lib/legend-ai/stores/LegendAIJoinAnalysis.d.ts +29 -0
  47. package/lib/legend-ai/stores/LegendAIJoinAnalysis.d.ts.map +1 -0
  48. package/lib/legend-ai/stores/LegendAIJoinAnalysis.js +124 -0
  49. package/lib/legend-ai/stores/LegendAIJoinAnalysis.js.map +1 -0
  50. package/lib/legend-ai/stores/LegendAISqlHelpers.d.ts +25 -0
  51. package/lib/legend-ai/stores/LegendAISqlHelpers.d.ts.map +1 -0
  52. package/lib/legend-ai/stores/LegendAISqlHelpers.js +64 -0
  53. package/lib/legend-ai/stores/LegendAISqlHelpers.js.map +1 -0
  54. package/lib/legend-ai/stores/LegendAISqlJoinSanitizers.d.ts +22 -0
  55. package/lib/legend-ai/stores/LegendAISqlJoinSanitizers.d.ts.map +1 -0
  56. package/lib/legend-ai/stores/LegendAISqlJoinSanitizers.js +411 -0
  57. package/lib/legend-ai/stores/LegendAISqlJoinSanitizers.js.map +1 -0
  58. package/package.json +6 -6
  59. package/src/legend-ai/LegendAIDocEnrichment.ts +435 -97
  60. package/src/legend-ai/LegendAIHostIntegration.tsx +80 -0
  61. package/src/legend-ai/LegendAIServiceRetrieval.ts +50 -35
  62. package/src/legend-ai/LegendAITypes.ts +169 -5
  63. package/src/legend-ai/LegendAI_LegendApplicationPlugin_Extension.ts +73 -1
  64. package/src/legend-ai/__test-utils__/LegendAITestUtils.ts +10 -0
  65. package/src/legend-ai/components/LegendAICharts.tsx +30 -33
  66. package/src/legend-ai/components/LegendAIChat.tsx +555 -91
  67. package/src/legend-ai/components/LegendAIChatInput.tsx +6 -2
  68. package/src/legend-ai/index.ts +22 -3
  69. package/src/legend-ai/stores/LegendAIChatProcessors.ts +1286 -322
  70. package/src/legend-ai/stores/LegendAIChatState.ts +73 -28
  71. package/src/legend-ai/stores/LegendAIJoinAnalysis.ts +165 -0
  72. package/src/legend-ai/stores/LegendAISqlHelpers.ts +82 -0
  73. package/src/legend-ai/stores/LegendAISqlJoinSanitizers.ts +515 -0
  74. package/tsconfig.json +4 -0
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Copyright (c) 2026-present, Goldman Sachs
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { useCallback } from 'react';
18
+ import { assertErrorThrown } from '@finos/legend-shared';
19
+ import {
20
+ type LegendAIChatTelemetryEvent,
21
+ type LegendAIChatTelemetryEventType,
22
+ type LegendAIPrimitiveValue,
23
+ type TDSServiceSchema,
24
+ logLegendAIChatTelemetry,
25
+ } from './LegendAITypes.js';
26
+
27
+ /**
28
+ * Loads a host's TDS service schemas and pushes them into React state, guarding
29
+ * against a resolve that lands after unmount. Returns the effect cleanup that
30
+ * cancels a pending load. Host effects supply their own dependency array.
31
+ */
32
+ export function bridgeLegendAIServices(
33
+ loadServices: () => Promise<TDSServiceSchema[]>,
34
+ onServicesLoaded: (services: TDSServiceSchema[]) => void,
35
+ onError: (error: Error) => void,
36
+ ): () => void {
37
+ let cancelled = false;
38
+ loadServices()
39
+ .then((services) => {
40
+ if (!cancelled) {
41
+ onServicesLoaded(services);
42
+ }
43
+ })
44
+ .catch((error) => {
45
+ assertErrorThrown(error);
46
+ onError(error);
47
+ });
48
+ return () => {
49
+ cancelled = true;
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Builds the host-agnostic telemetry callback that maps a chat telemetry event
55
+ * to a concrete host event key and forwards it with the shared context. Hosts
56
+ * provide their own event-key record and telemetry sink.
57
+ */
58
+ export function useLegendAIChatTelemetryLogger(
59
+ eventKeys: Record<LegendAIChatTelemetryEventType, string>,
60
+ context: string,
61
+ dataProduct: string,
62
+ telemetryService: {
63
+ logEvent: (
64
+ eventName: string,
65
+ payload: Record<string, LegendAIPrimitiveValue>,
66
+ ) => void;
67
+ },
68
+ ): (event: LegendAIChatTelemetryEvent) => void {
69
+ return useCallback(
70
+ (event: LegendAIChatTelemetryEvent): void => {
71
+ logLegendAIChatTelemetry(
72
+ event,
73
+ eventKeys,
74
+ { context, data_product: dataProduct },
75
+ (eventName, payload) => telemetryService.logEvent(eventName, payload),
76
+ );
77
+ },
78
+ [eventKeys, context, dataProduct, telemetryService],
79
+ );
80
+ }
@@ -25,6 +25,7 @@ const BM25_K1 = 1.2;
25
25
  const BM25_B = 0.75;
26
26
  const FIELD_WEIGHT_TITLE = 3;
27
27
  const FIELD_WEIGHT_DESCRIPTION = 2;
28
+ const FIELD_WEIGHT_DEFAULT = 1;
28
29
  const MIN_FUZZY_TOKEN_LENGTH = 4;
29
30
 
30
31
  interface BM25Doc {
@@ -49,30 +50,42 @@ function addTokensToDoc(doc: BM25Doc, tokens: string[], weight: number): void {
49
50
  }
50
51
  }
51
52
 
52
- function buildServiceDoc(svc: TDSServiceSchema): BM25Doc {
53
+ function buildServiceDoc(service: TDSServiceSchema): BM25Doc {
53
54
  const doc: BM25Doc = { termFreq: new Map(), length: 0 };
54
55
 
55
- addTokensToDoc(doc, tokensFromIdentifier(svc.title), FIELD_WEIGHT_TITLE);
56
- if (svc.description) {
56
+ addTokensToDoc(doc, tokensFromIdentifier(service.title), FIELD_WEIGHT_TITLE);
57
+ if (service.description) {
57
58
  addTokensToDoc(
58
59
  doc,
59
- tokenizeText(svc.description),
60
+ tokenizeText(service.description),
60
61
  FIELD_WEIGHT_DESCRIPTION,
61
62
  );
62
63
  }
63
- for (const col of svc.columns) {
64
- addTokensToDoc(doc, tokensFromIdentifier(col.name), 1);
65
- if (col.documentation) {
66
- addTokensToDoc(doc, tokenizeText(col.documentation), 1);
64
+ for (const column of service.columns) {
65
+ addTokensToDoc(
66
+ doc,
67
+ tokensFromIdentifier(column.name),
68
+ FIELD_WEIGHT_DEFAULT,
69
+ );
70
+ if (column.documentation) {
71
+ addTokensToDoc(
72
+ doc,
73
+ tokenizeText(column.documentation),
74
+ FIELD_WEIGHT_DEFAULT,
75
+ );
67
76
  }
68
77
  }
69
- for (const param of svc.parameters) {
70
- addTokensToDoc(doc, tokensFromIdentifier(param), 1);
78
+ for (const parameter of service.parameters) {
79
+ addTokensToDoc(doc, tokensFromIdentifier(parameter), FIELD_WEIGHT_DEFAULT);
71
80
  }
72
- for (const pf of svc.preFilters ?? []) {
73
- addTokensToDoc(doc, tokenizeText(pf.property), 1);
74
- if (pf.value !== undefined) {
75
- addTokensToDoc(doc, tokenizeText(String(pf.value)), 1);
81
+ for (const preFilter of service.preFilters ?? []) {
82
+ addTokensToDoc(doc, tokenizeText(preFilter.property), FIELD_WEIGHT_DEFAULT);
83
+ if (preFilter.value !== undefined) {
84
+ addTokensToDoc(
85
+ doc,
86
+ tokenizeText(String(preFilter.value)),
87
+ FIELD_WEIGHT_DEFAULT,
88
+ );
76
89
  }
77
90
  }
78
91
  return doc;
@@ -96,7 +109,10 @@ function buildBM25Index(services: readonly TDSServiceSchema[]): BM25Index {
96
109
  }
97
110
 
98
111
  /**
99
- * Standard Okapi BM25:
112
+ * Weighted Okapi BM25 variant — field weights fold into both the term frequency
113
+ * and the document length |D| (see `addTokensToDoc`), so |D| is a weight-inflated
114
+ * token count rather than a raw one. tf and length-normalization stay internally
115
+ * consistent.
100
116
  * IDF(q) = ln((N - df + 0.5) / (df + 0.5) + 1)
101
117
  * tfNorm = tf * (k1 + 1) / (tf + k1 * (1 - b + b * |D| / avgdl))
102
118
  * score = Σ IDF(q) * tfNorm
@@ -124,7 +140,6 @@ function scoreBM25(
124
140
  return score;
125
141
  }
126
142
 
127
- /** Rewrites a typoed query term to its closest indexed vocabulary entry. */
128
143
  function resolveQueryTermViaFuzzy(token: string, index: BM25Index): string {
129
144
  if (token.length < MIN_FUZZY_TOKEN_LENGTH || index.docFreq.has(token)) {
130
145
  return token;
@@ -154,17 +169,22 @@ export function preFilterServicesByRelevance(
154
169
  return services.slice(0, limit);
155
170
  }
156
171
  const index = buildBM25Index(services);
157
- const queryTerms = rawTerms.map((t) => resolveQueryTermViaFuzzy(t, index));
158
- const scored = services.map((svc, i) => ({
159
- svc,
172
+ const queryTerms = rawTerms.map((term) =>
173
+ resolveQueryTermViaFuzzy(term, index),
174
+ );
175
+ const scored = services.map((service, serviceIndex) => ({
176
+ service,
160
177
  score: scoreBM25(
161
178
  queryTerms,
162
- guaranteeNonNullable(index.docs[i], `BM25 doc missing for service ${i}`),
179
+ guaranteeNonNullable(
180
+ index.docs[serviceIndex],
181
+ `BM25 doc missing for service ${serviceIndex}`,
182
+ ),
163
183
  index,
164
184
  ),
165
185
  }));
166
186
  scored.sort((a, b) => b.score - a.score);
167
- return scored.slice(0, limit).map((s) => s.svc);
187
+ return scored.slice(0, limit).map((entry) => entry.service);
168
188
  }
169
189
 
170
190
  /**
@@ -195,25 +215,20 @@ export function levenshteinDistance(a: string, b: string): number {
195
215
  if (b.length === 0) {
196
216
  return a.length;
197
217
  }
198
- if (a.length > b.length) {
199
- const tmp = a;
200
- a = b;
201
- b = tmp;
202
- }
203
-
204
- const aLen = a.length;
205
- const bLen = b.length;
206
- const row = new Array<number>(aLen + 1);
218
+ const [shorter, longer] = a.length > b.length ? [b, a] : [a, b];
219
+ const shortLen = shorter.length;
220
+ const longLen = longer.length;
221
+ const row = new Array<number>(shortLen + 1);
207
222
 
208
- for (let i = 0; i <= aLen; i++) {
223
+ for (let i = 0; i <= shortLen; i++) {
209
224
  row[i] = i;
210
225
  }
211
226
 
212
- for (let j = 1; j <= bLen; j++) {
227
+ for (let j = 1; j <= longLen; j++) {
213
228
  let prev = guaranteeNonNullable(row[0]);
214
229
  row[0] = j;
215
- for (let i = 1; i <= aLen; i++) {
216
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
230
+ for (let i = 1; i <= shortLen; i++) {
231
+ const cost = shorter[i - 1] === longer[j - 1] ? 0 : 1;
217
232
  const current = guaranteeNonNullable(row[i]);
218
233
  row[i] = Math.min(
219
234
  current + 1, // deletion
@@ -224,5 +239,5 @@ export function levenshteinDistance(a: string, b: string): number {
224
239
  }
225
240
  }
226
241
 
227
- return guaranteeNonNullable(row[aLen]);
242
+ return guaranteeNonNullable(row[shortLen]);
228
243
  }
@@ -29,6 +29,7 @@ import type { LegendApplicationPlugin } from '@finos/legend-application';
29
29
  import {
30
30
  LegendAI_LegendApplicationPlugin_Extension,
31
31
  type LegendAIOrchestratorDataProductCoordinates,
32
+ type LegendAIResolvedEntities,
32
33
  } from './LegendAI_LegendApplicationPlugin_Extension.js';
33
34
 
34
35
  export class TDSColumnSchema {
@@ -48,6 +49,9 @@ export class TDSParameterSchema {
48
49
  required?: boolean;
49
50
  }
50
51
 
52
+ /** A non-null scalar value carried by pre-filters and telemetry payloads. */
53
+ export type LegendAIPrimitiveValue = string | number | boolean;
54
+
51
55
  /**
52
56
  * Describes a hardcoded filter constraint baked into a service lambda.
53
57
  * These are equality checks, isEmpty/isNotEmpty checks, or isNotNull
@@ -59,7 +63,7 @@ export interface TDSServicePreFilter {
59
63
  /** The comparison operator. */
60
64
  operator: 'equal' | 'isEmpty' | 'isNotEmpty' | 'isNotNull';
61
65
  /** The literal value for equality comparisons. */
62
- value?: string | number | boolean;
66
+ value?: LegendAIPrimitiveValue;
63
67
  }
64
68
 
65
69
  export enum TDSServiceSourceType {
@@ -117,6 +121,11 @@ export class LegendAIConfig {
117
121
  maxJudgeAttempts?: number;
118
122
  /** Lakehouse runtime environment name (e.g. 'prod', 'uat'). Defaults to 'prod' when not set. */
119
123
  lakehouseEnvironment?: string;
124
+ /**
125
+ * Ingest environment classification ('prod' | 'prod-parallel' | 'dev') —
126
+ * supplies the realm tier for lakehouse Python codegen.
127
+ */
128
+ lakehouseEnvironmentClassification?: string;
120
129
  /** URL to EngHub documentation for Legend AI setup. */
121
130
  enghubDocUrl?: string;
122
131
  /** URL to EntHub request access page for the AI coverage app. */
@@ -151,10 +160,14 @@ export function getTodayISO(): string {
151
160
  return new Date().toISOString().slice(0, 10);
152
161
  }
153
162
 
154
- /** Action ID used to offer the Legend AI Orchestrator as a fallback. */
155
163
  export const LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID =
156
164
  'orchestrator-fallback';
157
165
 
166
+ export const LEGEND_AI_ALTERNATE_ROOT_ACTION_ID = 'alternate-root';
167
+
168
+ export const LEGEND_AI_FEEDBACK_PROMPT = 'Was this helpful?';
169
+ export const LEGEND_AI_DEFAULT_MODEL_LABEL = 'Auto';
170
+
158
171
  export function findLegendAIPlugin(
159
172
  plugins: LegendApplicationPlugin[],
160
173
  ): LegendAI_LegendApplicationPlugin_Extension | undefined {
@@ -217,6 +230,14 @@ export class LegendAIUnsupportedEngineShapeError extends LegendAIServiceError {
217
230
  }
218
231
  }
219
232
 
233
+ export class LegendAIExecutionTimeoutError extends LegendAIServiceError {
234
+ override name = 'LegendAIExecutionTimeoutError';
235
+
236
+ constructor(hint: string) {
237
+ super(hint, LegendAIErrorType.EXECUTION);
238
+ }
239
+ }
240
+
220
241
  export enum LegendAIMessageRole {
221
242
  USER = 'user',
222
243
  ASSISTANT = 'assistant',
@@ -231,6 +252,7 @@ export class LegendAIUserMessage {
231
252
  export interface LegendAIFallbackAction {
232
253
  label: string;
233
254
  actionId: string;
255
+ resolvedEntities?: LegendAIResolvedEntities;
234
256
  }
235
257
 
236
258
  export enum LegendAIMessageFeedbackRating {
@@ -260,6 +282,39 @@ export enum LegendAIResponseOutcome {
260
282
  export enum LegendAIChatTelemetryEventType {
261
283
  QUESTION_ASKED = 'question-asked',
262
284
  RESPONSE_RECEIVED = 'response-received',
285
+ ASSISTANT_CLOSED = 'assistant-closed',
286
+ SUGGESTED_QUERY_CLICKED = 'suggested-query-clicked',
287
+ SCOPE_CHANGED = 'scope-changed',
288
+ MODEL_CHANGED = 'model-changed',
289
+ SQL_DETAILS_TOGGLED = 'sql-details-toggled',
290
+ ARTIFACT_COPIED = 'artifact-copied',
291
+ PYTHON_CODE_REQUESTED = 'python-code-requested',
292
+ PYTHON_CODE_TOGGLED = 'python-code-toggled',
293
+ OPEN_IN_DATACUBE_CLICKED = 'open-in-datacube-clicked',
294
+ }
295
+
296
+ export enum LegendAITelemetryArtifact {
297
+ SQL = 'sql',
298
+ PYTHON = 'python',
299
+ }
300
+
301
+ export enum LegendAISuggestedQuerySource {
302
+ STARTER = 'starter',
303
+ FOLLOW_UP = 'follow_up',
304
+ }
305
+
306
+ /** Buckets a row count into a coarse range so telemetry never carries exact sizes. */
307
+ export function toRowCountBucket(rowCount: number): string {
308
+ if (rowCount <= 0) {
309
+ return '0';
310
+ }
311
+ if (rowCount <= 10) {
312
+ return '1-10';
313
+ }
314
+ if (rowCount <= 100) {
315
+ return '11-100';
316
+ }
317
+ return '100+';
263
318
  }
264
319
 
265
320
  /**
@@ -279,7 +334,98 @@ export type LegendAIChatTelemetryEvent =
279
334
  type: LegendAIChatTelemetryEventType.RESPONSE_RECEIVED;
280
335
  outcome: LegendAIResponseOutcome;
281
336
  durationMs: number;
282
- };
337
+ hasSql: boolean;
338
+ hasResultGrid: boolean;
339
+ rowCountBucket: string;
340
+ suggestionCount: number;
341
+ errorType?: string;
342
+ }
343
+ | { type: LegendAIChatTelemetryEventType.ASSISTANT_CLOSED }
344
+ | {
345
+ type: LegendAIChatTelemetryEventType.SUGGESTED_QUERY_CLICKED;
346
+ position: number;
347
+ source: LegendAISuggestedQuerySource;
348
+ }
349
+ | {
350
+ type: LegendAIChatTelemetryEventType.SCOPE_CHANGED;
351
+ selectedCount: number;
352
+ includesMcp: boolean;
353
+ }
354
+ | {
355
+ type: LegendAIChatTelemetryEventType.MODEL_CHANGED;
356
+ model: string;
357
+ }
358
+ | {
359
+ type: LegendAIChatTelemetryEventType.SQL_DETAILS_TOGGLED;
360
+ shown: boolean;
361
+ }
362
+ | {
363
+ type: LegendAIChatTelemetryEventType.ARTIFACT_COPIED;
364
+ artifact: LegendAITelemetryArtifact;
365
+ }
366
+ | { type: LegendAIChatTelemetryEventType.PYTHON_CODE_REQUESTED }
367
+ | {
368
+ type: LegendAIChatTelemetryEventType.PYTHON_CODE_TOGGLED;
369
+ shown: boolean;
370
+ }
371
+ | { type: LegendAIChatTelemetryEventType.OPEN_IN_DATACUBE_CLICKED };
372
+
373
+ /** The event-specific (non-base) telemetry payload fields for a chat event. */
374
+ function chatTelemetryEventPayload(
375
+ event: LegendAIChatTelemetryEvent,
376
+ ): Record<string, LegendAIPrimitiveValue> {
377
+ switch (event.type) {
378
+ case LegendAIChatTelemetryEventType.QUESTION_ASKED:
379
+ return { question_length: event.questionLength };
380
+ case LegendAIChatTelemetryEventType.RESPONSE_RECEIVED:
381
+ return {
382
+ outcome: event.outcome,
383
+ duration_ms: event.durationMs,
384
+ has_sql: event.hasSql,
385
+ has_result_grid: event.hasResultGrid,
386
+ row_count_bucket: event.rowCountBucket,
387
+ suggestion_count: event.suggestionCount,
388
+ ...(event.errorType === undefined
389
+ ? {}
390
+ : { error_type: event.errorType }),
391
+ };
392
+ case LegendAIChatTelemetryEventType.SUGGESTED_QUERY_CLICKED:
393
+ return { position: event.position, source: event.source };
394
+ case LegendAIChatTelemetryEventType.SCOPE_CHANGED:
395
+ return {
396
+ selected_count: event.selectedCount,
397
+ includes_mcp: event.includesMcp,
398
+ };
399
+ case LegendAIChatTelemetryEventType.MODEL_CHANGED:
400
+ return { model: event.model };
401
+ case LegendAIChatTelemetryEventType.SQL_DETAILS_TOGGLED:
402
+ case LegendAIChatTelemetryEventType.PYTHON_CODE_TOGGLED:
403
+ return { shown: event.shown };
404
+ case LegendAIChatTelemetryEventType.ARTIFACT_COPIED:
405
+ return { artifact: event.artifact };
406
+ case LegendAIChatTelemetryEventType.ASSISTANT_CLOSED:
407
+ case LegendAIChatTelemetryEventType.PYTHON_CODE_REQUESTED:
408
+ case LegendAIChatTelemetryEventType.OPEN_IN_DATACUBE_CLICKED:
409
+ return {};
410
+ default:
411
+ return event satisfies never;
412
+ }
413
+ }
414
+
415
+ export function logLegendAIChatTelemetry(
416
+ event: LegendAIChatTelemetryEvent,
417
+ eventKeys: Record<LegendAIChatTelemetryEventType, string>,
418
+ base: Record<string, LegendAIPrimitiveValue>,
419
+ logEvent: (
420
+ eventName: string,
421
+ payload: Record<string, LegendAIPrimitiveValue>,
422
+ ) => void,
423
+ ): void {
424
+ logEvent(eventKeys[event.type], {
425
+ ...base,
426
+ ...chatTelemetryEventPayload(event),
427
+ });
428
+ }
283
429
 
284
430
  export class LegendAIAssistantMessage {
285
431
  id!: string;
@@ -299,6 +445,7 @@ export class LegendAIAssistantMessage {
299
445
  suggestedQueries!: string[];
300
446
  fallbackAction!: LegendAIFallbackAction | null;
301
447
  queriedAccessPointGroups!: string[];
448
+ queriedAccessPoints!: string[];
302
449
  }
303
450
 
304
451
  export type LegendAIMessage = LegendAIUserMessage | LegendAIAssistantMessage;
@@ -315,6 +462,7 @@ export class LegendAIConversationTurn {
315
462
  rowCount?: number;
316
463
  /** Brief summary of the result (e.g. column names, first few values). */
317
464
  resultSummary?: string;
465
+ queriedAccessPoints?: string[];
318
466
  }
319
467
 
320
468
  export interface LegendAIChatState {
@@ -427,7 +575,6 @@ export interface LegendAIModelEntity {
427
575
  properties: LegendAIModelProperty[];
428
576
  isRootMapped?: boolean;
429
577
  isQueryable?: boolean;
430
- superTypes?: string[];
431
578
  }
432
579
 
433
580
  export interface LegendAIModelAssociation {
@@ -459,17 +606,24 @@ export interface LegendAIExecutableInfo {
459
606
  description?: string;
460
607
  rootEntityPath: string;
461
608
  referencedEntityPaths?: string[];
462
- columns?: string[];
463
609
  queryTemplate?: string;
464
610
  requiredParameters?: LegendAIParameterInfo[];
465
611
  columnPropertyMappings?: LegendAIColumnPropertyMapping[];
466
612
  }
467
613
 
614
+ export interface LegendAIPhysicalDataset {
615
+ name: string;
616
+ database?: string;
617
+ schema?: string;
618
+ table?: string;
619
+ }
620
+
468
621
  export interface LegendAIModelContext {
469
622
  entities: LegendAIModelEntity[];
470
623
  associations: LegendAIModelAssociation[];
471
624
  enumerations?: LegendAIEnumerationInfo[];
472
625
  executables?: LegendAIExecutableInfo[];
626
+ datasets?: LegendAIPhysicalDataset[];
473
627
  dataspaceDescription?: string;
474
628
  }
475
629
 
@@ -748,6 +902,12 @@ export async function extractParameterSchemas(
748
902
  }
749
903
  }
750
904
 
905
+ export enum LegendAISelfHealKind {
906
+ NONE = 'none',
907
+ DISTINCT_VALUE = 'distinct-value',
908
+ LLM_ZERO_ROW = 'llm-zero-row',
909
+ }
910
+
751
911
  export interface LegendAIChatProps {
752
912
  services: TDSServiceSchema[];
753
913
  coordinates: string;
@@ -780,6 +940,10 @@ export interface LegendAIChatProps {
780
940
  feedback: LegendAIMessageFeedback,
781
941
  ) => Promise<void> | void;
782
942
  onRequestAccess?: (accessPointGroupTitle: string) => void;
943
+ onOpenInDataCube?: (
944
+ accessPointName: string,
945
+ pureQuery: string | undefined,
946
+ ) => void;
783
947
  contextBannerMessage?: string;
784
948
  /**
785
949
  * Optional usage-analytics sink. Fired when the user asks a question and when
@@ -18,6 +18,7 @@ import {
18
18
  LegendApplicationPlugin,
19
19
  type LegendApplicationPluginManager,
20
20
  } from '@finos/legend-application';
21
+ import { UnsupportedOperationError } from '@finos/legend-shared';
21
22
  import type {
22
23
  QueryExplicitExecutionContextInfo,
23
24
  TDSRowDataType,
@@ -148,6 +149,28 @@ export class LegendAIResultAnalysis {
148
149
  suggestedQueries!: string[];
149
150
  }
150
151
 
152
+ export class LegendAIPythonQueryCode {
153
+ code!: string;
154
+ notebookUrl?: string;
155
+ }
156
+
157
+ export interface LegendAIDataCubeQueryTranslationRequest {
158
+ sql: string;
159
+ service: TDSServiceSchema;
160
+ dataProductPath: string;
161
+ config: LegendAIConfig;
162
+ question?: string;
163
+ }
164
+
165
+ export interface LegendAIPythonCodegenRequest {
166
+ service: TDSServiceSchema;
167
+ dataProductCoordinates?: LegendAIOrchestratorDataProductCoordinates;
168
+ config: LegendAIConfig;
169
+ question?: string;
170
+ sql?: string;
171
+ history?: LegendAIConversationTurn[];
172
+ }
173
+
151
174
  /**
152
175
  * @deprecated Use {@link QueryExplicitExecutionContextInfo} from `@finos/legend-graph` directly.
153
176
  */
@@ -338,6 +361,22 @@ export abstract class LegendAI_LegendApplicationPlugin_Extension extends LegendA
338
361
  config: LegendAIConfig,
339
362
  ): Promise<LegendAISqlExecutionResultData>;
340
363
 
364
+ /**
365
+ * Execute a Pure relation query built on a data-product accessor
366
+ * (e.g. `#P{path.accessPoint}#->select(~[col])->distinct()->take(n)`) against
367
+ * the lakehouse and return tabular results. Access-point-capable plugins
368
+ * override this; the default signals the capability is unavailable.
369
+ */
370
+ executeLakehouseRelationQuery(
371
+ _relationQuery: string,
372
+ _dataProductCoordinates: LegendAIOrchestratorDataProductCoordinates,
373
+ _config: LegendAIConfig,
374
+ ): Promise<LegendAISqlExecutionResultData> {
375
+ throw new UnsupportedOperationError(
376
+ `Executing lakehouse relation queries is not supported by this plugin`,
377
+ );
378
+ }
379
+
341
380
  /**
342
381
  * Analyze query results using the LLM to produce a summary, chart
343
382
  * recommendation, key metrics, and suggested follow-up queries.
@@ -349,16 +388,21 @@ export abstract class LegendAI_LegendApplicationPlugin_Extension extends LegendA
349
388
  rows: TDSRowDataType[],
350
389
  metadata: LegendAIProductMetadata,
351
390
  config: LegendAIConfig,
391
+ totalRowCount: number,
392
+ dataAggregates: string,
352
393
  ): Promise<LegendAIResultAnalysis | undefined>;
353
394
 
354
395
  /**
355
- * Build a fallback analysis when the query returns no results.
396
+ * Build a fallback analysis when the query returns no results. `diagnosis`,
397
+ * when provided, is a deterministically computed reason (e.g. disjoint join
398
+ * keys) the LLM should phrase rather than guess.
356
399
  */
357
400
  abstract buildNoResultsFallback(
358
401
  question: string,
359
402
  query: string,
360
403
  metadata: LegendAIProductMetadata,
361
404
  config: LegendAIConfig,
405
+ diagnosis?: string,
362
406
  ): Promise<LegendAIResultAnalysis | undefined>;
363
407
 
364
408
  /**
@@ -425,6 +469,26 @@ export abstract class LegendAI_LegendApplicationPlugin_Extension extends LegendA
425
469
  return Promise.resolve(services);
426
470
  }
427
471
 
472
+ supportsPythonCodegen(_service: TDSServiceSchema): boolean {
473
+ return false;
474
+ }
475
+
476
+ generatePythonQueryCodeAsync(
477
+ _request: LegendAIPythonCodegenRequest,
478
+ ): Promise<LegendAIPythonQueryCode | undefined> {
479
+ return Promise.resolve(undefined);
480
+ }
481
+
482
+ supportsOpenInDataCube(_service: TDSServiceSchema): boolean {
483
+ return false;
484
+ }
485
+
486
+ translateAccessPointSqlToDataCubeQuery(
487
+ _request: LegendAIDataCubeQueryTranslationRequest,
488
+ ): Promise<string | undefined> {
489
+ return Promise.resolve(undefined);
490
+ }
491
+
428
492
  /**
429
493
  * Probe a service by executing SELECT * LIMIT 1 to discover the real column
430
494
  * names from the engine. Returns the actual column names or undefined if
@@ -438,6 +502,14 @@ export abstract class LegendAI_LegendApplicationPlugin_Extension extends LegendA
438
502
  return Promise.resolve(undefined);
439
503
  }
440
504
 
505
+ enrichAccessPointSampleValues(
506
+ _accessPoints: TDSServiceSchema[],
507
+ _dataProductCoordinates: LegendAIOrchestratorDataProductCoordinates,
508
+ _config: LegendAIConfig,
509
+ ): Promise<void> {
510
+ return Promise.resolve();
511
+ }
512
+
441
513
  /**
442
514
  * Build an error correction prompt that feeds the execution error back to
443
515
  * the LLM and asks it to fix the SQL. Used in the execution retry loop.
@@ -74,6 +74,7 @@ export const TEST__makeAssistantMessage = (
74
74
  suggestedQueries: [],
75
75
  fallbackAction: null,
76
76
  queriedAccessPointGroups: [],
77
+ queriedAccessPoints: [],
77
78
  ...overrides,
78
79
  });
79
80
  export const TEST__seedAssistant = (setter: MessageSetter): void => {
@@ -175,6 +176,9 @@ export const TEST__createMockLegendAIPlugin = (
175
176
  preWarmSchemaAnalysis: () => {
176
177
  /* no-op in test */
177
178
  },
179
+ enrichAccessPointSampleValues: (): Promise<void> => Promise.resolve(),
180
+ buildNoResultsFallback: (): Promise<undefined> =>
181
+ Promise.resolve(undefined),
178
182
  callLLM: createMock(),
179
183
  executeSql: createMock(),
180
184
  extractSqlFromResponse: (_a: string): LegendAISqlExtractionResult => ({
@@ -213,6 +217,12 @@ export const TEST__createMockLegendAIPlugin = (
213
217
  },
214
218
  buildErrorCorrectionPrompt: (): string => '',
215
219
  buildZeroRowCorrectionPrompt: (): string => '',
220
+ supportsPythonCodegen: (): boolean => false,
221
+ generatePythonQueryCodeAsync: (): Promise<undefined> =>
222
+ Promise.resolve(undefined),
223
+ supportsOpenInDataCube: (): boolean => false,
224
+ translateAccessPointSqlToDataCubeQuery: (): Promise<undefined> =>
225
+ Promise.resolve(undefined),
216
226
  ...overrides,
217
227
  }) as LegendAI_LegendApplicationPlugin_Extension;
218
228