@finos/legend-lego 2.0.202 → 2.0.203
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.css +2 -2
- package/lib/index.css.map +1 -1
- package/lib/legend-ai/LegendAIDocEnrichment.d.ts +34 -1
- package/lib/legend-ai/LegendAIDocEnrichment.d.ts.map +1 -1
- package/lib/legend-ai/LegendAIDocEnrichment.js +890 -1
- package/lib/legend-ai/LegendAIDocEnrichment.js.map +1 -1
- package/lib/legend-ai/LegendAIServiceRetrieval.d.ts +36 -0
- package/lib/legend-ai/LegendAIServiceRetrieval.d.ts.map +1 -0
- package/lib/legend-ai/LegendAIServiceRetrieval.js +179 -0
- package/lib/legend-ai/LegendAIServiceRetrieval.js.map +1 -0
- package/lib/legend-ai/LegendAITypes.d.ts +62 -2
- package/lib/legend-ai/LegendAITypes.d.ts.map +1 -1
- package/lib/legend-ai/LegendAITypes.js +56 -30
- package/lib/legend-ai/LegendAITypes.js.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts +29 -6
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js +9 -0
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js.map +1 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.d.ts +5 -0
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.d.ts.map +1 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.js +18 -2
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.js.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisPanel.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisPanel.js +27 -8
- package/lib/legend-ai/components/LegendAIAnalysisPanel.js.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts +1 -0
- package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisUtils.js +144 -22
- package/lib/legend-ai/components/LegendAIAnalysisUtils.js.map +1 -1
- package/lib/legend-ai/components/LegendAICharts.d.ts +4 -0
- package/lib/legend-ai/components/LegendAICharts.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAICharts.js +60 -1
- package/lib/legend-ai/components/LegendAICharts.js.map +1 -1
- package/lib/legend-ai/components/LegendAIChat.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIChat.js +46 -17
- package/lib/legend-ai/components/LegendAIChat.js.map +1 -1
- package/lib/legend-ai/components/LegendAIChatHelpers.d.ts +1 -1
- package/lib/legend-ai/components/LegendAIChatHelpers.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIChatHelpers.js +220 -15
- package/lib/legend-ai/components/LegendAIChatHelpers.js.map +1 -1
- package/lib/legend-ai/components/LegendAIChatInput.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIChatInput.js +1 -0
- package/lib/legend-ai/components/LegendAIChatInput.js.map +1 -1
- package/lib/legend-ai/components/LegendAIScopeSelector.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIScopeSelector.js +5 -2
- package/lib/legend-ai/components/LegendAIScopeSelector.js.map +1 -1
- package/lib/legend-ai/index.d.ts +4 -3
- package/lib/legend-ai/index.d.ts.map +1 -1
- package/lib/legend-ai/index.js +4 -3
- package/lib/legend-ai/index.js.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts +30 -13
- package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatProcessors.js +776 -216
- package/lib/legend-ai/stores/LegendAIChatProcessors.js.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatState.d.ts +2 -2
- package/lib/legend-ai/stores/LegendAIChatState.d.ts.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatState.js +26 -25
- package/lib/legend-ai/stores/LegendAIChatState.js.map +1 -1
- package/package.json +3 -3
- package/src/legend-ai/LegendAIDocEnrichment.ts +1198 -1
- package/src/legend-ai/LegendAIServiceRetrieval.ts +228 -0
- package/src/legend-ai/LegendAITypes.ts +138 -32
- package/src/legend-ai/LegendAI_LegendApplicationPlugin_Extension.ts +62 -0
- package/src/legend-ai/__test-utils__/LegendAITestUtils.ts +28 -0
- package/src/legend-ai/components/LegendAIAnalysisPanel.tsx +36 -17
- package/src/legend-ai/components/LegendAIAnalysisUtils.ts +178 -22
- package/src/legend-ai/components/LegendAICharts.tsx +160 -0
- package/src/legend-ai/components/LegendAIChat.tsx +107 -14
- package/src/legend-ai/components/LegendAIChatHelpers.ts +334 -20
- package/src/legend-ai/components/LegendAIChatInput.tsx +2 -1
- package/src/legend-ai/components/LegendAIScopeSelector.tsx +7 -2
- package/src/legend-ai/index.ts +22 -1
- package/src/legend-ai/stores/LegendAIChatProcessors.ts +1205 -321
- package/src/legend-ai/stores/LegendAIChatState.ts +80 -69
- package/tsconfig.json +1 -0
|
@@ -0,0 +1,228 @@
|
|
|
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 { guaranteeNonNullable } from '@finos/legend-shared';
|
|
18
|
+
import type { TDSServiceSchema } from './LegendAITypes.js';
|
|
19
|
+
import {
|
|
20
|
+
splitIdentifierTokens,
|
|
21
|
+
tokenizeText,
|
|
22
|
+
} from './LegendAIDocEnrichment.js';
|
|
23
|
+
|
|
24
|
+
const BM25_K1 = 1.2;
|
|
25
|
+
const BM25_B = 0.75;
|
|
26
|
+
const FIELD_WEIGHT_TITLE = 3;
|
|
27
|
+
const FIELD_WEIGHT_DESCRIPTION = 2;
|
|
28
|
+
const MIN_FUZZY_TOKEN_LENGTH = 4;
|
|
29
|
+
|
|
30
|
+
interface BM25Doc {
|
|
31
|
+
termFreq: Map<string, number>;
|
|
32
|
+
length: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface BM25Index {
|
|
36
|
+
docs: BM25Doc[];
|
|
37
|
+
docFreq: Map<string, number>;
|
|
38
|
+
avgLength: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function tokensFromIdentifier(name: string): string[] {
|
|
42
|
+
return [...tokenizeText(name), ...splitIdentifierTokens(name)];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function addTokensToDoc(doc: BM25Doc, tokens: string[], weight: number): void {
|
|
46
|
+
for (const token of tokens) {
|
|
47
|
+
doc.termFreq.set(token, (doc.termFreq.get(token) ?? 0) + weight);
|
|
48
|
+
doc.length += weight;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildServiceDoc(svc: TDSServiceSchema): BM25Doc {
|
|
53
|
+
const doc: BM25Doc = { termFreq: new Map(), length: 0 };
|
|
54
|
+
|
|
55
|
+
addTokensToDoc(doc, tokensFromIdentifier(svc.title), FIELD_WEIGHT_TITLE);
|
|
56
|
+
if (svc.description) {
|
|
57
|
+
addTokensToDoc(
|
|
58
|
+
doc,
|
|
59
|
+
tokenizeText(svc.description),
|
|
60
|
+
FIELD_WEIGHT_DESCRIPTION,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
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);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (const param of svc.parameters) {
|
|
70
|
+
addTokensToDoc(doc, tokensFromIdentifier(param), 1);
|
|
71
|
+
}
|
|
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);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return doc;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function buildBM25Index(services: readonly TDSServiceSchema[]): BM25Index {
|
|
82
|
+
const docs = services.map(buildServiceDoc);
|
|
83
|
+
const docFreq = new Map<string, number>();
|
|
84
|
+
let totalLength = 0;
|
|
85
|
+
for (const doc of docs) {
|
|
86
|
+
totalLength += doc.length;
|
|
87
|
+
for (const term of doc.termFreq.keys()) {
|
|
88
|
+
docFreq.set(term, (docFreq.get(term) ?? 0) + 1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
docs,
|
|
93
|
+
docFreq,
|
|
94
|
+
avgLength: docs.length === 0 ? 0 : totalLength / docs.length,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Standard Okapi BM25:
|
|
100
|
+
* IDF(q) = ln((N - df + 0.5) / (df + 0.5) + 1)
|
|
101
|
+
* tfNorm = tf * (k1 + 1) / (tf + k1 * (1 - b + b * |D| / avgdl))
|
|
102
|
+
* score = Σ IDF(q) * tfNorm
|
|
103
|
+
*/
|
|
104
|
+
function scoreBM25(
|
|
105
|
+
queryTerms: string[],
|
|
106
|
+
doc: BM25Doc,
|
|
107
|
+
index: BM25Index,
|
|
108
|
+
): number {
|
|
109
|
+
const n = index.docs.length;
|
|
110
|
+
const avgdl = index.avgLength === 0 ? 1 : index.avgLength;
|
|
111
|
+
let score = 0;
|
|
112
|
+
for (const term of queryTerms) {
|
|
113
|
+
const tf = doc.termFreq.get(term);
|
|
114
|
+
if (!tf) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const df = index.docFreq.get(term) ?? 0;
|
|
118
|
+
const idf = Math.log((n - df + 0.5) / (df + 0.5) + 1);
|
|
119
|
+
const tfNorm =
|
|
120
|
+
(tf * (BM25_K1 + 1)) /
|
|
121
|
+
(tf + BM25_K1 * (1 - BM25_B + (BM25_B * doc.length) / avgdl));
|
|
122
|
+
score += idf * tfNorm;
|
|
123
|
+
}
|
|
124
|
+
return score;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Rewrites a typoed query term to its closest indexed vocabulary entry. */
|
|
128
|
+
function resolveQueryTermViaFuzzy(token: string, index: BM25Index): string {
|
|
129
|
+
if (token.length < MIN_FUZZY_TOKEN_LENGTH || index.docFreq.has(token)) {
|
|
130
|
+
return token;
|
|
131
|
+
}
|
|
132
|
+
for (const term of index.docFreq.keys()) {
|
|
133
|
+
if (term.length >= MIN_FUZZY_TOKEN_LENGTH && isFuzzyMatch(token, term)) {
|
|
134
|
+
return term;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return token;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Lexical pre-filter that ranks services against the user question using
|
|
142
|
+
* Okapi BM25. IDF down-weights corpus-wide common terms and length
|
|
143
|
+
* normalization demotes denormalized aggregate tables — no stopword list
|
|
144
|
+
* or specificity penalty needed. Query typos are recovered via
|
|
145
|
+
* {@link resolveQueryTermViaFuzzy} before scoring.
|
|
146
|
+
*/
|
|
147
|
+
export function preFilterServicesByRelevance(
|
|
148
|
+
question: string,
|
|
149
|
+
services: TDSServiceSchema[],
|
|
150
|
+
limit: number,
|
|
151
|
+
): TDSServiceSchema[] {
|
|
152
|
+
const rawTerms = tokenizeText(question);
|
|
153
|
+
if (rawTerms.length === 0) {
|
|
154
|
+
return services.slice(0, limit);
|
|
155
|
+
}
|
|
156
|
+
const index = buildBM25Index(services);
|
|
157
|
+
const queryTerms = rawTerms.map((t) => resolveQueryTermViaFuzzy(t, index));
|
|
158
|
+
const scored = services.map((svc, i) => ({
|
|
159
|
+
svc,
|
|
160
|
+
score: scoreBM25(
|
|
161
|
+
queryTerms,
|
|
162
|
+
guaranteeNonNullable(index.docs[i], `BM25 doc missing for service ${i}`),
|
|
163
|
+
index,
|
|
164
|
+
),
|
|
165
|
+
}));
|
|
166
|
+
scored.sort((a, b) => b.score - a.score);
|
|
167
|
+
return scored.slice(0, limit).map((s) => s.svc);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Returns `true` when two tokens are within Levenshtein edit distance 1 (for
|
|
172
|
+
* short tokens) or 2 (for longer tokens). Used to recover typoed query terms
|
|
173
|
+
* during retrieval and coverage checks.
|
|
174
|
+
*/
|
|
175
|
+
export function isFuzzyMatch(a: string, b: string): boolean {
|
|
176
|
+
const lenDiff = Math.abs(a.length - b.length);
|
|
177
|
+
if (lenDiff > 2) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
const maxDist = Math.min(a.length, b.length) <= 4 ? 1 : 2;
|
|
181
|
+
return levenshteinDistance(a, b) <= maxDist;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Computes the Levenshtein edit distance between two strings.
|
|
186
|
+
* Uses a single-row DP approach for space efficiency.
|
|
187
|
+
*/
|
|
188
|
+
export function levenshteinDistance(a: string, b: string): number {
|
|
189
|
+
if (a === b) {
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
if (a.length === 0) {
|
|
193
|
+
return b.length;
|
|
194
|
+
}
|
|
195
|
+
if (b.length === 0) {
|
|
196
|
+
return a.length;
|
|
197
|
+
}
|
|
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);
|
|
207
|
+
|
|
208
|
+
for (let i = 0; i <= aLen; i++) {
|
|
209
|
+
row[i] = i;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (let j = 1; j <= bLen; j++) {
|
|
213
|
+
let prev = guaranteeNonNullable(row[0]);
|
|
214
|
+
row[0] = j;
|
|
215
|
+
for (let i = 1; i <= aLen; i++) {
|
|
216
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
217
|
+
const current = guaranteeNonNullable(row[i]);
|
|
218
|
+
row[i] = Math.min(
|
|
219
|
+
current + 1, // deletion
|
|
220
|
+
guaranteeNonNullable(row[i - 1]) + 1, // insertion
|
|
221
|
+
prev + cost, // substitution
|
|
222
|
+
);
|
|
223
|
+
prev = current;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return guaranteeNonNullable(row[aLen]);
|
|
228
|
+
}
|
|
@@ -209,6 +209,14 @@ export class LegendAIServiceError extends Error {
|
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
export class LegendAIUnsupportedEngineShapeError extends LegendAIServiceError {
|
|
213
|
+
override name = 'LegendAIUnsupportedEngineShapeError';
|
|
214
|
+
|
|
215
|
+
constructor(hint: string) {
|
|
216
|
+
super(hint, LegendAIErrorType.EXECUTION);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
212
220
|
export enum LegendAIMessageRole {
|
|
213
221
|
USER = 'user',
|
|
214
222
|
ASSISTANT = 'assistant',
|
|
@@ -256,6 +264,7 @@ export class LegendAIAssistantMessage {
|
|
|
256
264
|
isExecuting!: boolean;
|
|
257
265
|
suggestedQueries!: string[];
|
|
258
266
|
fallbackAction!: LegendAIFallbackAction | null;
|
|
267
|
+
queriedAccessPointGroups!: string[];
|
|
259
268
|
}
|
|
260
269
|
|
|
261
270
|
export type LegendAIMessage = LegendAIUserMessage | LegendAIAssistantMessage;
|
|
@@ -366,6 +375,70 @@ export class LegendAIProductMetadata {
|
|
|
366
375
|
domainContext?: string;
|
|
367
376
|
}
|
|
368
377
|
|
|
378
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
379
|
+
// Model context for local entity resolution (DataSpaces only)
|
|
380
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
381
|
+
|
|
382
|
+
export interface LegendAIModelProperty {
|
|
383
|
+
name: string;
|
|
384
|
+
type: string;
|
|
385
|
+
isCollection: boolean;
|
|
386
|
+
isOptional: boolean;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export interface LegendAIModelEntity {
|
|
390
|
+
path: string;
|
|
391
|
+
name: string;
|
|
392
|
+
description?: string;
|
|
393
|
+
properties: LegendAIModelProperty[];
|
|
394
|
+
isRootMapped?: boolean;
|
|
395
|
+
isQueryable?: boolean;
|
|
396
|
+
superTypes?: string[];
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export interface LegendAIModelAssociation {
|
|
400
|
+
name: string;
|
|
401
|
+
leftEntity: string;
|
|
402
|
+
leftProperty: string;
|
|
403
|
+
rightEntity: string;
|
|
404
|
+
rightProperty: string;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export interface LegendAIColumnPropertyMapping {
|
|
408
|
+
columnName: string;
|
|
409
|
+
propertyPath: string;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export interface LegendAIParameterInfo {
|
|
413
|
+
name: string;
|
|
414
|
+
type: string;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export interface LegendAIEnumerationInfo {
|
|
418
|
+
path: string;
|
|
419
|
+
name: string;
|
|
420
|
+
values: string[];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export interface LegendAIExecutableInfo {
|
|
424
|
+
title: string;
|
|
425
|
+
description?: string;
|
|
426
|
+
rootEntityPath: string;
|
|
427
|
+
referencedEntityPaths?: string[];
|
|
428
|
+
columns?: string[];
|
|
429
|
+
queryTemplate?: string;
|
|
430
|
+
requiredParameters?: LegendAIParameterInfo[];
|
|
431
|
+
columnPropertyMappings?: LegendAIColumnPropertyMapping[];
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export interface LegendAIModelContext {
|
|
435
|
+
entities: LegendAIModelEntity[];
|
|
436
|
+
associations: LegendAIModelAssociation[];
|
|
437
|
+
enumerations?: LegendAIEnumerationInfo[];
|
|
438
|
+
executables?: LegendAIExecutableInfo[];
|
|
439
|
+
dataspaceDescription?: string;
|
|
440
|
+
}
|
|
441
|
+
|
|
369
442
|
export enum LegendAIQuestionIntent {
|
|
370
443
|
DATA_QUERY = 'data_query',
|
|
371
444
|
METADATA = 'metadata',
|
|
@@ -474,6 +547,58 @@ export class QuestionIntentClassification {
|
|
|
474
547
|
ambiguous!: boolean;
|
|
475
548
|
}
|
|
476
549
|
|
|
550
|
+
function maskEntityNames(question: string, entityNames: string[]): string {
|
|
551
|
+
const escaped = entityNames
|
|
552
|
+
.filter((name) => name.length > 0)
|
|
553
|
+
.map((name) =>
|
|
554
|
+
name.toLowerCase().replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`),
|
|
555
|
+
);
|
|
556
|
+
if (escaped.length === 0) {
|
|
557
|
+
return question;
|
|
558
|
+
}
|
|
559
|
+
return question.replaceAll(new RegExp(escaped.join('|'), 'g'), '___');
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function classifyMixedSignal(
|
|
563
|
+
q: string,
|
|
564
|
+
metaScore: number,
|
|
565
|
+
dataScore: number,
|
|
566
|
+
): QuestionIntentClassification {
|
|
567
|
+
if (dataScore >= metaScore * 2) {
|
|
568
|
+
return {
|
|
569
|
+
intent: LegendAIQuestionIntent.DATA_QUERY,
|
|
570
|
+
metaScore,
|
|
571
|
+
dataScore,
|
|
572
|
+
ambiguous: false,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
if (PRODUCT_REFERENCE_PATTERN.test(q) || STRUCTURAL_KEYWORD_PATTERN.test(q)) {
|
|
576
|
+
return {
|
|
577
|
+
intent: LegendAIQuestionIntent.METADATA,
|
|
578
|
+
metaScore,
|
|
579
|
+
dataScore,
|
|
580
|
+
ambiguous: false,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
if (CAPABILITY_DISCOVERY_PATTERNS.some((p) => p.test(q))) {
|
|
584
|
+
return {
|
|
585
|
+
intent: LegendAIQuestionIntent.METADATA,
|
|
586
|
+
metaScore,
|
|
587
|
+
dataScore,
|
|
588
|
+
ambiguous: false,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
intent:
|
|
593
|
+
metaScore > dataScore
|
|
594
|
+
? LegendAIQuestionIntent.METADATA
|
|
595
|
+
: LegendAIQuestionIntent.DATA_QUERY,
|
|
596
|
+
metaScore,
|
|
597
|
+
dataScore,
|
|
598
|
+
ambiguous: true,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
477
602
|
/**
|
|
478
603
|
* Fast deterministic regex classifier (sync, < 1ms).
|
|
479
604
|
* Returns both the resolved intent AND whether the result is ambiguous,
|
|
@@ -482,6 +607,7 @@ export class QuestionIntentClassification {
|
|
|
482
607
|
export function classifyQuestionIntentFast(
|
|
483
608
|
question: string,
|
|
484
609
|
hasServices: boolean,
|
|
610
|
+
entityNames?: string[],
|
|
485
611
|
): QuestionIntentClassification {
|
|
486
612
|
const q = question.toLowerCase().trim();
|
|
487
613
|
|
|
@@ -495,7 +621,12 @@ export function classifyQuestionIntentFast(
|
|
|
495
621
|
}
|
|
496
622
|
|
|
497
623
|
const metaScore = countPatternMatches(q, METADATA_SIGNAL_PATTERNS);
|
|
498
|
-
const
|
|
624
|
+
const qForDataScore =
|
|
625
|
+
entityNames && entityNames.length > 0 ? maskEntityNames(q, entityNames) : q;
|
|
626
|
+
const dataScore = countPatternMatches(
|
|
627
|
+
qForDataScore,
|
|
628
|
+
DATA_QUERY_SIGNAL_PATTERNS,
|
|
629
|
+
);
|
|
499
630
|
|
|
500
631
|
if (metaScore > 0 && dataScore === 0) {
|
|
501
632
|
const isStructural =
|
|
@@ -518,37 +649,8 @@ export function classifyQuestionIntentFast(
|
|
|
518
649
|
ambiguous: false,
|
|
519
650
|
};
|
|
520
651
|
}
|
|
521
|
-
|
|
522
652
|
if (metaScore > 0 && dataScore > 0) {
|
|
523
|
-
|
|
524
|
-
return {
|
|
525
|
-
intent: LegendAIQuestionIntent.DATA_QUERY,
|
|
526
|
-
metaScore,
|
|
527
|
-
dataScore,
|
|
528
|
-
ambiguous: false,
|
|
529
|
-
};
|
|
530
|
-
}
|
|
531
|
-
if (
|
|
532
|
-
PRODUCT_REFERENCE_PATTERN.test(q) ||
|
|
533
|
-
STRUCTURAL_KEYWORD_PATTERN.test(q)
|
|
534
|
-
) {
|
|
535
|
-
return {
|
|
536
|
-
intent: LegendAIQuestionIntent.METADATA,
|
|
537
|
-
metaScore,
|
|
538
|
-
dataScore,
|
|
539
|
-
ambiguous: false,
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
const tentative =
|
|
543
|
-
metaScore > dataScore
|
|
544
|
-
? LegendAIQuestionIntent.METADATA
|
|
545
|
-
: LegendAIQuestionIntent.DATA_QUERY;
|
|
546
|
-
return {
|
|
547
|
-
intent: tentative,
|
|
548
|
-
metaScore,
|
|
549
|
-
dataScore,
|
|
550
|
-
ambiguous: true,
|
|
551
|
-
};
|
|
653
|
+
return classifyMixedSignal(q, metaScore, dataScore);
|
|
552
654
|
}
|
|
553
655
|
|
|
554
656
|
return {
|
|
@@ -568,8 +670,9 @@ export function classifyQuestionIntentFast(
|
|
|
568
670
|
export function classifyQuestionIntent(
|
|
569
671
|
question: string,
|
|
570
672
|
hasServices: boolean,
|
|
673
|
+
entityNames?: string[],
|
|
571
674
|
): LegendAIQuestionIntent {
|
|
572
|
-
return classifyQuestionIntentFast(question, hasServices).intent;
|
|
675
|
+
return classifyQuestionIntentFast(question, hasServices, entityNames).intent;
|
|
573
676
|
}
|
|
574
677
|
|
|
575
678
|
export async function extractParameterSchemas(
|
|
@@ -629,6 +732,7 @@ export interface LegendAIChatProps {
|
|
|
629
732
|
* returned by the orchestrator. Required for the execute-after-generate flow.
|
|
630
733
|
*/
|
|
631
734
|
pureExecutionContext?: QueryExplicitExecutionContextInfo;
|
|
735
|
+
modelContext?: LegendAIModelContext;
|
|
632
736
|
availableScopes?: LegendAIScopeItem[];
|
|
633
737
|
/** Called when the user clicks the close button in the chat header. */
|
|
634
738
|
onClose?: () => void;
|
|
@@ -641,4 +745,6 @@ export interface LegendAIChatProps {
|
|
|
641
745
|
onMessageFeedback?: (
|
|
642
746
|
feedback: LegendAIMessageFeedback,
|
|
643
747
|
) => Promise<void> | void;
|
|
748
|
+
onRequestAccess?: (accessPointGroupTitle: string) => void;
|
|
749
|
+
contextBannerMessage?: string;
|
|
644
750
|
}
|
|
@@ -28,6 +28,7 @@ import type {
|
|
|
28
28
|
LegendAIConversationTurn,
|
|
29
29
|
LegendAIProductMetadata,
|
|
30
30
|
LegendAIQuestionIntent,
|
|
31
|
+
LegendAIModelContext,
|
|
31
32
|
} from './LegendAITypes.js';
|
|
32
33
|
|
|
33
34
|
export class LegendAISqlExtractionResult {
|
|
@@ -63,6 +64,30 @@ export interface LegendAISemanticSearchResolutionDetails {
|
|
|
63
64
|
data_product_coordinates: LegendAIOrchestratorDataProductCoordinates;
|
|
64
65
|
root_entity: string;
|
|
65
66
|
related_entities: string[];
|
|
67
|
+
enriched_business_context?: LegendAIEnrichedBusinessContext;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface LegendAIEnrichedBusinessContext {
|
|
71
|
+
naturalLanguageQuery?: string;
|
|
72
|
+
businessContextMatch?: LegendAIBusinessContextMatch;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface LegendAIBusinessContextMatch {
|
|
76
|
+
properties?: LegendAIBusinessContextProperty[];
|
|
77
|
+
additionalNlModelContext?: LegendAIAdditionalNlModelContext[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface LegendAIBusinessContextProperty {
|
|
81
|
+
propertyName: string;
|
|
82
|
+
matchType?: string[];
|
|
83
|
+
propertyValues?: string[];
|
|
84
|
+
probablePropertyValues?: string[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface LegendAIAdditionalNlModelContext {
|
|
88
|
+
id: string;
|
|
89
|
+
description: string;
|
|
90
|
+
category: string;
|
|
66
91
|
}
|
|
67
92
|
|
|
68
93
|
export class LegendAIOrchestratorRequest {
|
|
@@ -161,6 +186,8 @@ export abstract class LegendAI_LegendApplicationPlugin_Extension extends LegendA
|
|
|
161
186
|
question: string,
|
|
162
187
|
metadata: LegendAIProductMetadata,
|
|
163
188
|
history?: LegendAIConversationTurn[],
|
|
189
|
+
services?: TDSServiceSchema[],
|
|
190
|
+
modelContextEnrichment?: string,
|
|
164
191
|
): string;
|
|
165
192
|
|
|
166
193
|
/**
|
|
@@ -172,6 +199,7 @@ export abstract class LegendAI_LegendApplicationPlugin_Extension extends LegendA
|
|
|
172
199
|
coordinates: string,
|
|
173
200
|
history?: LegendAIConversationTurn[],
|
|
174
201
|
metadata?: LegendAIProductMetadata,
|
|
202
|
+
modelContextEnrichment?: string,
|
|
175
203
|
): string;
|
|
176
204
|
|
|
177
205
|
/**
|
|
@@ -194,6 +222,7 @@ export abstract class LegendAI_LegendApplicationPlugin_Extension extends LegendA
|
|
|
194
222
|
question: string,
|
|
195
223
|
accessPoints: TDSServiceSchema[],
|
|
196
224
|
history?: LegendAIConversationTurn[],
|
|
225
|
+
modelContextEnrichment?: string,
|
|
197
226
|
): string;
|
|
198
227
|
|
|
199
228
|
/**
|
|
@@ -205,8 +234,40 @@ export abstract class LegendAI_LegendApplicationPlugin_Extension extends LegendA
|
|
|
205
234
|
question: string,
|
|
206
235
|
accessPoints: TDSServiceSchema[],
|
|
207
236
|
history?: LegendAIConversationTurn[],
|
|
237
|
+
modelContextEnrichment?: string,
|
|
208
238
|
): string;
|
|
209
239
|
|
|
240
|
+
buildAccessPointErrorCorrectionPrompt(
|
|
241
|
+
failedSql: string,
|
|
242
|
+
question: string,
|
|
243
|
+
executionError: string,
|
|
244
|
+
accessPoints: TDSServiceSchema[],
|
|
245
|
+
history?: LegendAIConversationTurn[],
|
|
246
|
+
modelContextEnrichment?: string,
|
|
247
|
+
): string {
|
|
248
|
+
return this.buildAccessPointJudgePrompt(
|
|
249
|
+
failedSql,
|
|
250
|
+
`${question}\n\nEXECUTION ERROR — the previous SQL failed. Fix it using the SAME access points:\n${executionError}`,
|
|
251
|
+
accessPoints,
|
|
252
|
+
history,
|
|
253
|
+
modelContextEnrichment,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
normalizeQuestion(
|
|
258
|
+
question: string,
|
|
259
|
+
_config: LegendAIConfig,
|
|
260
|
+
): Promise<string> {
|
|
261
|
+
return Promise.resolve(question);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
preWarmSchemaAnalysis(
|
|
265
|
+
_services: TDSServiceSchema[],
|
|
266
|
+
_config: LegendAIConfig,
|
|
267
|
+
): void {
|
|
268
|
+
/* no-op by default — plugins override */
|
|
269
|
+
}
|
|
270
|
+
|
|
210
271
|
/**
|
|
211
272
|
* Send a prompt to the LLM service and return the raw response text.
|
|
212
273
|
* The plugin manages conversation lifecycle internally — callers
|
|
@@ -252,6 +313,7 @@ export abstract class LegendAI_LegendApplicationPlugin_Extension extends LegendA
|
|
|
252
313
|
dataProductCoordinates: LegendAIOrchestratorDataProductCoordinates,
|
|
253
314
|
config: LegendAIConfig,
|
|
254
315
|
pureExecutionContext?: QueryExplicitExecutionContextInfo,
|
|
316
|
+
modelContext?: LegendAIModelContext,
|
|
255
317
|
): Promise<LegendAIResolvedEntities>;
|
|
256
318
|
|
|
257
319
|
/**
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { guaranteeNonNullable } from '@finos/legend-shared';
|
|
17
18
|
import { createMock } from '@finos/legend-shared/test';
|
|
18
19
|
import type {
|
|
19
20
|
MessageSetter,
|
|
@@ -72,6 +73,7 @@ export const TEST__makeAssistantMessage = (
|
|
|
72
73
|
isExecuting: false,
|
|
73
74
|
suggestedQueries: [],
|
|
74
75
|
fallbackAction: null,
|
|
76
|
+
queriedAccessPointGroups: [],
|
|
75
77
|
...overrides,
|
|
76
78
|
});
|
|
77
79
|
export const TEST__seedAssistant = (setter: MessageSetter): void => {
|
|
@@ -80,6 +82,27 @@ export const TEST__seedAssistant = (setter: MessageSetter): void => {
|
|
|
80
82
|
TEST__makeAssistantMessage(),
|
|
81
83
|
]);
|
|
82
84
|
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Retrieve a message by index and assert it is an assistant message,
|
|
88
|
+
* narrowing the type via the discriminated `role` field without a cast.
|
|
89
|
+
*/
|
|
90
|
+
export const TEST__getAssistantMessage = (
|
|
91
|
+
messages: LegendAIMessage[],
|
|
92
|
+
index: number,
|
|
93
|
+
): LegendAIAssistantMessage => {
|
|
94
|
+
const msg = guaranteeNonNullable(
|
|
95
|
+
messages[index],
|
|
96
|
+
`Expected message at index ${index}`,
|
|
97
|
+
);
|
|
98
|
+
if (msg.role !== LegendAIMessageRole.ASSISTANT) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Expected assistant message at index ${index}, got role '${msg.role}'`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return msg;
|
|
104
|
+
};
|
|
105
|
+
|
|
83
106
|
export const TEST_DATA__legendAIConfig: LegendAIConfig = {
|
|
84
107
|
enabled: true,
|
|
85
108
|
llmServiceUrl: 'http://localhost/llm',
|
|
@@ -140,13 +163,18 @@ export const TEST__createMockLegendAIPlugin = (
|
|
|
140
163
|
_q: string,
|
|
141
164
|
_s: TDSServiceSchema[],
|
|
142
165
|
_h?: LegendAIConversationTurn[],
|
|
166
|
+
_m?: string,
|
|
143
167
|
) => 'ap generator prompt',
|
|
144
168
|
buildAccessPointJudgePrompt: (
|
|
145
169
|
_sql: string,
|
|
146
170
|
_q: string,
|
|
147
171
|
_s: TDSServiceSchema[],
|
|
148
172
|
_h?: LegendAIConversationTurn[],
|
|
173
|
+
_m?: string,
|
|
149
174
|
) => 'ap judge prompt',
|
|
175
|
+
preWarmSchemaAnalysis: () => {
|
|
176
|
+
/* no-op in test */
|
|
177
|
+
},
|
|
150
178
|
callLLM: createMock(),
|
|
151
179
|
executeSql: createMock(),
|
|
152
180
|
extractSqlFromResponse: (_a: string): LegendAISqlExtractionResult => ({
|