@finos/legend-lego 2.0.211 → 2.0.213
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.css +2 -2
- package/lib/index.css.map +1 -1
- package/lib/legend-ai/LegendAIDocEnrichment.d.ts +12 -12
- package/lib/legend-ai/LegendAIDocEnrichment.d.ts.map +1 -1
- package/lib/legend-ai/LegendAIDocEnrichment.js +317 -86
- package/lib/legend-ai/LegendAIDocEnrichment.js.map +1 -1
- package/lib/legend-ai/LegendAIHostIntegration.d.ts +31 -0
- package/lib/legend-ai/LegendAIHostIntegration.d.ts.map +1 -0
- package/lib/legend-ai/LegendAIHostIntegration.js +50 -0
- package/lib/legend-ai/LegendAIHostIntegration.js.map +1 -0
- package/lib/legend-ai/LegendAIServiceRetrieval.d.ts.map +1 -1
- package/lib/legend-ai/LegendAIServiceRetrieval.js +34 -35
- package/lib/legend-ai/LegendAIServiceRetrieval.js.map +1 -1
- package/lib/legend-ai/LegendAITypes.d.ts +85 -7
- package/lib/legend-ai/LegendAITypes.d.ts.map +1 -1
- package/lib/legend-ai/LegendAITypes.js +99 -1
- package/lib/legend-ai/LegendAITypes.js.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts +36 -3
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js +29 -0
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js.map +1 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.d.ts.map +1 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.js +7 -0
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.js.map +1 -1
- package/lib/legend-ai/components/LegendAICharts.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAICharts.js +6 -10
- package/lib/legend-ai/components/LegendAICharts.js.map +1 -1
- package/lib/legend-ai/components/LegendAIChat.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIChat.js +263 -41
- package/lib/legend-ai/components/LegendAIChat.js.map +1 -1
- package/lib/legend-ai/components/LegendAIChatInput.d.ts +1 -1
- package/lib/legend-ai/components/LegendAIChatInput.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIChatInput.js +2 -1
- package/lib/legend-ai/components/LegendAIChatInput.js.map +1 -1
- package/lib/legend-ai/index.d.ts +5 -2
- package/lib/legend-ai/index.d.ts.map +1 -1
- package/lib/legend-ai/index.js +5 -2
- package/lib/legend-ai/index.js.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts +9 -3
- package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatProcessors.js +794 -220
- package/lib/legend-ai/stores/LegendAIChatProcessors.js.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatState.d.ts.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatState.js +47 -21
- package/lib/legend-ai/stores/LegendAIChatState.js.map +1 -1
- package/lib/legend-ai/stores/LegendAIJoinAnalysis.d.ts +29 -0
- package/lib/legend-ai/stores/LegendAIJoinAnalysis.d.ts.map +1 -0
- package/lib/legend-ai/stores/LegendAIJoinAnalysis.js +124 -0
- package/lib/legend-ai/stores/LegendAIJoinAnalysis.js.map +1 -0
- package/lib/legend-ai/stores/LegendAISqlHelpers.d.ts +25 -0
- package/lib/legend-ai/stores/LegendAISqlHelpers.d.ts.map +1 -0
- package/lib/legend-ai/stores/LegendAISqlHelpers.js +64 -0
- package/lib/legend-ai/stores/LegendAISqlHelpers.js.map +1 -0
- package/lib/legend-ai/stores/LegendAISqlJoinSanitizers.d.ts +22 -0
- package/lib/legend-ai/stores/LegendAISqlJoinSanitizers.d.ts.map +1 -0
- package/lib/legend-ai/stores/LegendAISqlJoinSanitizers.js +411 -0
- package/lib/legend-ai/stores/LegendAISqlJoinSanitizers.js.map +1 -0
- package/package.json +6 -6
- package/src/legend-ai/LegendAIDocEnrichment.ts +435 -97
- package/src/legend-ai/LegendAIHostIntegration.tsx +80 -0
- package/src/legend-ai/LegendAIServiceRetrieval.ts +50 -35
- package/src/legend-ai/LegendAITypes.ts +169 -5
- package/src/legend-ai/LegendAI_LegendApplicationPlugin_Extension.ts +73 -1
- package/src/legend-ai/__test-utils__/LegendAITestUtils.ts +10 -0
- package/src/legend-ai/components/LegendAICharts.tsx +30 -33
- package/src/legend-ai/components/LegendAIChat.tsx +555 -91
- package/src/legend-ai/components/LegendAIChatInput.tsx +6 -2
- package/src/legend-ai/index.ts +22 -3
- package/src/legend-ai/stores/LegendAIChatProcessors.ts +1286 -322
- package/src/legend-ai/stores/LegendAIChatState.ts +73 -28
- package/src/legend-ai/stores/LegendAIJoinAnalysis.ts +165 -0
- package/src/legend-ai/stores/LegendAISqlHelpers.ts +82 -0
- package/src/legend-ai/stores/LegendAISqlJoinSanitizers.ts +515 -0
- package/tsconfig.json +4 -0
|
@@ -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(
|
|
53
|
+
function buildServiceDoc(service: TDSServiceSchema): BM25Doc {
|
|
53
54
|
const doc: BM25Doc = { termFreq: new Map(), length: 0 };
|
|
54
55
|
|
|
55
|
-
addTokensToDoc(doc, tokensFromIdentifier(
|
|
56
|
-
if (
|
|
56
|
+
addTokensToDoc(doc, tokensFromIdentifier(service.title), FIELD_WEIGHT_TITLE);
|
|
57
|
+
if (service.description) {
|
|
57
58
|
addTokensToDoc(
|
|
58
59
|
doc,
|
|
59
|
-
tokenizeText(
|
|
60
|
+
tokenizeText(service.description),
|
|
60
61
|
FIELD_WEIGHT_DESCRIPTION,
|
|
61
62
|
);
|
|
62
63
|
}
|
|
63
|
-
for (const
|
|
64
|
-
addTokensToDoc(
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
70
|
-
addTokensToDoc(doc, tokensFromIdentifier(
|
|
78
|
+
for (const parameter of service.parameters) {
|
|
79
|
+
addTokensToDoc(doc, tokensFromIdentifier(parameter), FIELD_WEIGHT_DEFAULT);
|
|
71
80
|
}
|
|
72
|
-
for (const
|
|
73
|
-
addTokensToDoc(doc, tokenizeText(
|
|
74
|
-
if (
|
|
75
|
-
addTokensToDoc(
|
|
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
|
-
*
|
|
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((
|
|
158
|
-
|
|
159
|
-
|
|
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(
|
|
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((
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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 <=
|
|
223
|
+
for (let i = 0; i <= shortLen; i++) {
|
|
209
224
|
row[i] = i;
|
|
210
225
|
}
|
|
211
226
|
|
|
212
|
-
for (let j = 1; 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 <=
|
|
216
|
-
const cost =
|
|
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[
|
|
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?:
|
|
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
|
|