@finos/legend-lego 2.0.197 → 2.0.198
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 +60 -0
- package/lib/legend-ai/LegendAIDocEnrichment.d.ts.map +1 -0
- package/lib/legend-ai/LegendAIDocEnrichment.js +429 -0
- package/lib/legend-ai/LegendAIDocEnrichment.js.map +1 -0
- package/lib/legend-ai/LegendAITypes.d.ts +127 -1
- package/lib/legend-ai/LegendAITypes.d.ts.map +1 -1
- package/lib/legend-ai/LegendAITypes.js +111 -2
- package/lib/legend-ai/LegendAITypes.js.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts +14 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js.map +1 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.d.ts +2 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.d.ts.map +1 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.js +37 -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 +11 -12
- package/lib/legend-ai/components/LegendAIAnalysisPanel.js.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts +7 -0
- package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIAnalysisUtils.js +106 -41
- package/lib/legend-ai/components/LegendAIAnalysisUtils.js.map +1 -1
- package/lib/legend-ai/components/LegendAIChat.d.ts +1 -5
- package/lib/legend-ai/components/LegendAIChat.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIChat.js +168 -109
- package/lib/legend-ai/components/LegendAIChat.js.map +1 -1
- package/lib/legend-ai/components/LegendAIChatHelpers.d.ts +21 -0
- package/lib/legend-ai/components/LegendAIChatHelpers.d.ts.map +1 -0
- package/lib/legend-ai/components/LegendAIChatHelpers.js +85 -0
- package/lib/legend-ai/components/LegendAIChatHelpers.js.map +1 -0
- package/lib/legend-ai/components/LegendAIChatInput.d.ts +21 -0
- package/lib/legend-ai/components/LegendAIChatInput.d.ts.map +1 -0
- package/lib/legend-ai/components/LegendAIChatInput.js +78 -0
- package/lib/legend-ai/components/LegendAIChatInput.js.map +1 -0
- package/lib/legend-ai/components/LegendAIScopeSelector.d.ts +25 -0
- package/lib/legend-ai/components/LegendAIScopeSelector.d.ts.map +1 -0
- package/lib/legend-ai/components/LegendAIScopeSelector.js +85 -0
- package/lib/legend-ai/components/LegendAIScopeSelector.js.map +1 -0
- package/lib/legend-ai/index.d.ts +8 -3
- package/lib/legend-ai/index.d.ts.map +1 -1
- package/lib/legend-ai/index.js +8 -3
- package/lib/legend-ai/index.js.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts +105 -0
- package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts.map +1 -0
- package/lib/legend-ai/stores/LegendAIChatProcessors.js +1482 -0
- package/lib/legend-ai/stores/LegendAIChatProcessors.js.map +1 -0
- package/lib/legend-ai/stores/LegendAIChatState.d.ts +2 -35
- package/lib/legend-ai/stores/LegendAIChatState.d.ts.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatState.js +114 -949
- package/lib/legend-ai/stores/LegendAIChatState.js.map +1 -1
- package/package.json +5 -5
- package/src/legend-ai/LegendAIDocEnrichment.ts +572 -0
- package/src/legend-ai/LegendAITypes.ts +213 -5
- package/src/legend-ai/LegendAI_LegendApplicationPlugin_Extension.ts +25 -0
- package/src/legend-ai/__test-utils__/LegendAITestUtils.ts +55 -1
- package/src/legend-ai/components/LegendAIAnalysisPanel.tsx +14 -34
- package/src/legend-ai/components/LegendAIAnalysisUtils.ts +157 -47
- package/src/legend-ai/components/LegendAIChat.tsx +389 -206
- package/src/legend-ai/components/LegendAIChatHelpers.ts +117 -0
- package/src/legend-ai/components/LegendAIChatInput.tsx +209 -0
- package/src/legend-ai/components/LegendAIScopeSelector.tsx +199 -0
- package/src/legend-ai/index.ts +31 -4
- package/src/legend-ai/stores/LegendAIChatProcessors.ts +2563 -0
- package/src/legend-ai/stores/LegendAIChatState.ts +161 -1697
- package/tsconfig.json +5 -0
|
@@ -15,10 +15,16 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { DataGridColumnDefinition } from '../data-grid/index.js';
|
|
18
|
-
import
|
|
19
|
-
TDSRowDataType,
|
|
20
|
-
QueryExplicitExecutionContextInfo,
|
|
18
|
+
import {
|
|
19
|
+
type TDSRowDataType,
|
|
20
|
+
type QueryExplicitExecutionContextInfo,
|
|
21
|
+
type AbstractPureGraphManager,
|
|
22
|
+
type GraphManagerState,
|
|
23
|
+
buildLambdaVariableExpressions,
|
|
24
|
+
VariableExpression,
|
|
25
|
+
extractElementNameFromPath,
|
|
21
26
|
} from '@finos/legend-graph';
|
|
27
|
+
import { filterByType } from '@finos/legend-shared';
|
|
22
28
|
import type { LegendApplicationPlugin } from '@finos/legend-application';
|
|
23
29
|
import {
|
|
24
30
|
LegendAI_LegendApplicationPlugin_Extension,
|
|
@@ -30,6 +36,30 @@ export class TDSColumnSchema {
|
|
|
30
36
|
type?: string;
|
|
31
37
|
documentation?: string;
|
|
32
38
|
sampleValues?: string;
|
|
39
|
+
/** Whether this column is nullable (multiplicity lowerBound === 0). */
|
|
40
|
+
nullable?: boolean;
|
|
41
|
+
/** Physical relational type (e.g. 'VARCHAR(100)', 'DECIMAL(18,4)'). */
|
|
42
|
+
relationalType?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class TDSParameterSchema {
|
|
46
|
+
name!: string;
|
|
47
|
+
type?: string;
|
|
48
|
+
required?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Describes a hardcoded filter constraint baked into a service lambda.
|
|
53
|
+
* These are equality checks, isEmpty/isNotEmpty checks, or isNotNull
|
|
54
|
+
* guards that are applied before the TDS result is returned.
|
|
55
|
+
*/
|
|
56
|
+
export interface TDSServicePreFilter {
|
|
57
|
+
/** Dot-separated property path (e.g. 'FeSecCoveragePublic.SymCoveragePublicEquities.SymSecEntityPublic.fsymId'). */
|
|
58
|
+
property: string;
|
|
59
|
+
/** The comparison operator. */
|
|
60
|
+
operator: 'equal' | 'isEmpty' | 'isNotEmpty' | 'isNotNull';
|
|
61
|
+
/** The literal value for equality comparisons. */
|
|
62
|
+
value?: string | number | boolean;
|
|
33
63
|
}
|
|
34
64
|
|
|
35
65
|
export enum TDSServiceSourceType {
|
|
@@ -43,6 +73,12 @@ export class TDSServiceSchema {
|
|
|
43
73
|
pattern!: string;
|
|
44
74
|
columns!: TDSColumnSchema[];
|
|
45
75
|
parameters!: string[];
|
|
76
|
+
/**
|
|
77
|
+
* Rich parameter metadata including type and multiplicity.
|
|
78
|
+
* When populated, downstream consumers can use this for richer
|
|
79
|
+
* prompts and user-facing hints. Falls back to `parameters` names.
|
|
80
|
+
*/
|
|
81
|
+
parameterSchemas?: TDSParameterSchema[];
|
|
46
82
|
/**
|
|
47
83
|
* Indicates the source of this service schema.
|
|
48
84
|
* - SERVICE: traditional DataSpace service executable (uses `FROM service(...)` SQL syntax)
|
|
@@ -51,12 +87,28 @@ export class TDSServiceSchema {
|
|
|
51
87
|
sourceType?: TDSServiceSourceType;
|
|
52
88
|
/** Full data product path (e.g. 'my::package::DataProduct'), used with `p()` syntax for access points. */
|
|
53
89
|
dataProductPath?: string;
|
|
90
|
+
/**
|
|
91
|
+
* Set to true when parameter extraction from the service query failed.
|
|
92
|
+
* Downstream consumers can use this to warn users that required parameters
|
|
93
|
+
* may not be detected automatically.
|
|
94
|
+
*/
|
|
95
|
+
parameterExtractionFailed?: boolean;
|
|
96
|
+
/** Access point group title this AP belongs to. */
|
|
97
|
+
accessPointGroupTitle?: string;
|
|
98
|
+
/** Raw DDL script (CREATE VIEW/TABLE) from the access point resource builder. */
|
|
99
|
+
ddlScript?: string;
|
|
100
|
+
/**
|
|
101
|
+
* Hardcoded filter constraints extracted from the service lambda.
|
|
102
|
+
* These indicate pre-applied conditions the AI must not contradict.
|
|
103
|
+
*/
|
|
104
|
+
preFilters?: TDSServicePreFilter[];
|
|
54
105
|
}
|
|
55
106
|
|
|
56
107
|
export class LegendAIConfig {
|
|
57
108
|
enabled!: boolean;
|
|
58
109
|
llmServiceUrl!: string | undefined;
|
|
59
110
|
llmModelName!: string | undefined;
|
|
111
|
+
llmModelOptions?: string[];
|
|
60
112
|
sqlExecutionUrl!: string | undefined;
|
|
61
113
|
orchestratorUrl!: string | undefined;
|
|
62
114
|
marketplaceSearchUrl!: string | undefined;
|
|
@@ -88,6 +140,17 @@ export const COVERAGE_NAME_PROD = 'legend-ai';
|
|
|
88
140
|
/** EngHub coverage app name for non-production (sandbox). */
|
|
89
141
|
export const COVERAGE_NAME_SANDBOX = 'Legend-AI-Sandbox';
|
|
90
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Delimiter used in TDS column `doc` fields to separate human-readable
|
|
145
|
+
* documentation from sample values. Shared across DataProduct and
|
|
146
|
+
* DataSpace parsers.
|
|
147
|
+
*/
|
|
148
|
+
export const TDS_SAMPLE_VALUES_DELIMITER = '-- e.g.';
|
|
149
|
+
|
|
150
|
+
export function getTodayISO(): string {
|
|
151
|
+
return new Date().toISOString().slice(0, 10);
|
|
152
|
+
}
|
|
153
|
+
|
|
91
154
|
/** Action ID used to offer the Legend AI Orchestrator as a fallback. */
|
|
92
155
|
export const LEGEND_AI_ORCHESTRATOR_FALLBACK_ACTION_ID =
|
|
93
156
|
'orchestrator-fallback';
|
|
@@ -162,6 +225,20 @@ export interface LegendAIFallbackAction {
|
|
|
162
225
|
actionId: string;
|
|
163
226
|
}
|
|
164
227
|
|
|
228
|
+
export enum LegendAIMessageFeedbackRating {
|
|
229
|
+
THUMBS_UP = 'thumbs_up',
|
|
230
|
+
THUMBS_DOWN = 'thumbs_down',
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export interface LegendAIMessageFeedback {
|
|
234
|
+
messageId: string;
|
|
235
|
+
rating: LegendAIMessageFeedbackRating;
|
|
236
|
+
question: string;
|
|
237
|
+
answer?: string;
|
|
238
|
+
sql?: string;
|
|
239
|
+
rowCount?: number;
|
|
240
|
+
}
|
|
241
|
+
|
|
165
242
|
export class LegendAIAssistantMessage {
|
|
166
243
|
id!: string;
|
|
167
244
|
role!: LegendAIMessageRole.ASSISTANT;
|
|
@@ -191,6 +268,10 @@ export class LegendAIConversationTurn {
|
|
|
191
268
|
* to differentiate prior metadata answers from SQL query results.
|
|
192
269
|
*/
|
|
193
270
|
intent?: LegendAIQuestionIntent;
|
|
271
|
+
/** Number of rows returned by the query, when available. */
|
|
272
|
+
rowCount?: number;
|
|
273
|
+
/** Brief summary of the result (e.g. column names, first few values). */
|
|
274
|
+
resultSummary?: string;
|
|
194
275
|
}
|
|
195
276
|
|
|
196
277
|
export interface LegendAIChatState {
|
|
@@ -198,6 +279,9 @@ export interface LegendAIChatState {
|
|
|
198
279
|
setQuestionText: (text: string) => void;
|
|
199
280
|
isSending: boolean;
|
|
200
281
|
messages: LegendAIMessage[];
|
|
282
|
+
selectedModelName: string | undefined;
|
|
283
|
+
availableModelNames: string[];
|
|
284
|
+
setSelectedModelName: (modelName: string) => void;
|
|
201
285
|
askQuestion: () => void;
|
|
202
286
|
askQuestionWithIntent: (text: string, intent: LegendAIQuestionIntent) => void;
|
|
203
287
|
runFallbackAction: (messageId: string) => void;
|
|
@@ -205,11 +289,23 @@ export interface LegendAIChatState {
|
|
|
205
289
|
expandedThinking: Set<number>;
|
|
206
290
|
toggleThinking: (index: number) => void;
|
|
207
291
|
conversationRef: { readonly current: HTMLDivElement | null };
|
|
292
|
+
selectedScopes: LegendAIScopeItem[];
|
|
293
|
+
toggleScope: (scope: LegendAIScopeItem) => void;
|
|
294
|
+
removeScope: (scopeId: string) => void;
|
|
295
|
+
stopGeneration: () => void;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export interface LegendAIScopeItem {
|
|
299
|
+
id: string;
|
|
300
|
+
label: string;
|
|
301
|
+
description?: string;
|
|
208
302
|
}
|
|
209
303
|
|
|
210
304
|
export interface LegendAIServiceSummary {
|
|
211
305
|
title: string;
|
|
212
306
|
description?: string;
|
|
307
|
+
columnNames?: string[];
|
|
308
|
+
parameters?: string[];
|
|
213
309
|
}
|
|
214
310
|
|
|
215
311
|
export interface LegendAIAccessPointInfo {
|
|
@@ -228,6 +324,32 @@ export class LegendAIAccessPointGroupInfo {
|
|
|
228
324
|
accessPoints!: LegendAIAccessPointInfo[];
|
|
229
325
|
}
|
|
230
326
|
|
|
327
|
+
export interface LegendAIAccessPointRelationship {
|
|
328
|
+
leftAccessPoint: string;
|
|
329
|
+
rightAccessPoint: string;
|
|
330
|
+
sharedColumns: string[];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Describes a relationship between two TDS services in a DataSpace,
|
|
335
|
+
* derived from model association documentation (elementDocs).
|
|
336
|
+
* Used to generate accurate JOIN hints in LLM prompts.
|
|
337
|
+
*/
|
|
338
|
+
export interface LegendAIServiceRelationship {
|
|
339
|
+
/** Title of the first service. */
|
|
340
|
+
leftService: string;
|
|
341
|
+
/** Title of the second service. */
|
|
342
|
+
rightService: string;
|
|
343
|
+
/** Column names that can serve as JOIN keys between the services. */
|
|
344
|
+
joinColumns: string[];
|
|
345
|
+
/** Name of the intermediate entity connecting both services (e.g. "Own2Ownermap"). */
|
|
346
|
+
viaEntity?: string;
|
|
347
|
+
/** Cardinality from the connecting entity to the left service (e.g. "1:*"). */
|
|
348
|
+
leftCardinality?: string;
|
|
349
|
+
/** Cardinality from the connecting entity to the right service (e.g. "1:*"). */
|
|
350
|
+
rightCardinality?: string;
|
|
351
|
+
}
|
|
352
|
+
|
|
231
353
|
export class LegendAIProductMetadata {
|
|
232
354
|
name!: string;
|
|
233
355
|
description?: string;
|
|
@@ -236,6 +358,12 @@ export class LegendAIProductMetadata {
|
|
|
236
358
|
accessPointGroups?: LegendAIAccessPointGroupInfo[];
|
|
237
359
|
tags?: LegendAITagInfo[];
|
|
238
360
|
supportInfo?: string;
|
|
361
|
+
/** Inferred cross-access-point relationships based on shared column names. */
|
|
362
|
+
accessPointRelationships?: LegendAIAccessPointRelationship[];
|
|
363
|
+
/** Cross-service relationships derived from model associations (elementDocs). */
|
|
364
|
+
serviceRelationships?: LegendAIServiceRelationship[];
|
|
365
|
+
/** Per-product domain knowledge for LLM context enrichment. */
|
|
366
|
+
domainContext?: string;
|
|
239
367
|
}
|
|
240
368
|
|
|
241
369
|
export enum LegendAIQuestionIntent {
|
|
@@ -269,12 +397,16 @@ export const METADATA_SIGNAL_PATTERNS: readonly RegExp[] = Object.freeze([
|
|
|
269
397
|
/\b(?:help\s+me\s+(?:understand|with))\b/,
|
|
270
398
|
/\bwhat\s+(?:information|data|content|datasets?)\s+(?:is|are|does)\b/,
|
|
271
399
|
/\b(?:what\s+does)\s+\S+(?:\s+\S+)*\s+(?:do|provide|offer|contain|include|cover)\b/,
|
|
400
|
+
/\b(?:how\s+are)\s+(?:these|the|those|this)\s+(?:\S+\s+)*(?:related|connected|linked|different|similar)\b/,
|
|
401
|
+
/\b(?:relationship|similarities|differences)\s+(?:between|across|among)\b/,
|
|
402
|
+
/\b(?:can\s+(?:we|i|you)\s+(?:join|combine|link|relate|connect))\s+(?:these|the|those|them)\b/,
|
|
272
403
|
]);
|
|
273
404
|
|
|
274
405
|
export const DATA_QUERY_SIGNAL_PATTERNS: readonly RegExp[] = Object.freeze([
|
|
275
406
|
/\b(?:select|query|sql|rows?|records?|count|sum|avg|average|min|max|total)\b/,
|
|
276
407
|
/\b(?:top\s+\d+|first\s+\d+|last\s+\d+|limit\s+\d+)\b/,
|
|
277
|
-
/\b(?:filter|where|group\s+by|order\s+by|sort|
|
|
408
|
+
/\b(?:filter|where|group\s+by|order\s+by|sort|aggregate)\b/,
|
|
409
|
+
/\bjoin\s+(?:on|using|between|the\s+(?:tables?|services?|data))\b/,
|
|
278
410
|
/\b(?:distinct|unique)\s+(?:values?|entries?|items?)/,
|
|
279
411
|
/\bfrom\s+(?:\S+\s+)*service\b/,
|
|
280
412
|
/\b(?:show|give|get|fetch|retrieve|pull|find|provide|display|return)\s+(?:me\s+)?/,
|
|
@@ -298,12 +430,25 @@ export const DATA_QUERY_SIGNAL_PATTERNS: readonly RegExp[] = Object.freeze([
|
|
|
298
430
|
/\b(?:grouped?\s+by|broken?\s+down\s+by|split\s+by|segmented?\s+by)\b/,
|
|
299
431
|
]);
|
|
300
432
|
|
|
433
|
+
const EXPLICIT_METADATA_OVERRIDE_PATTERNS: readonly RegExp[] = [
|
|
434
|
+
/\b(?:from|using|based\s+on|answer\s+from|just)\s+(?:the\s+)?metadata\b/,
|
|
435
|
+
/\b(?:don'?t|do\s+not|no)\s+(?:run\s+(?:a\s+)?)?(?:query|queries|sql|execute|fetch)\b/,
|
|
436
|
+
/\bjust\s+(?:answer|explain|describe|tell\s+me)\b/,
|
|
437
|
+
/\b(?:without|skip)\s+(?:querying|executing|running|fetching)\b/,
|
|
438
|
+
];
|
|
439
|
+
|
|
301
440
|
const PRODUCT_REFERENCE_PATTERN =
|
|
302
441
|
/\b(?:this|the)\s+(?:data\s*product|dataspace|data\s*space|product)\b/;
|
|
303
442
|
|
|
304
443
|
const STRUCTURAL_KEYWORD_PATTERN =
|
|
305
444
|
/\b(?:services?|endpoints?|access\s*points?|capabilities|owner|maintainer|support)\b/;
|
|
306
445
|
|
|
446
|
+
const CAPABILITY_DISCOVERY_PATTERNS: readonly RegExp[] = [
|
|
447
|
+
/\bwhat\s+data\s+does\b/,
|
|
448
|
+
/\bwhat\s+does\s+\S+(?:\s+\S+)*\s+offer\b/,
|
|
449
|
+
/\bhow\s+can\s+i\s+use\b/,
|
|
450
|
+
];
|
|
451
|
+
|
|
307
452
|
function countPatternMatches(
|
|
308
453
|
question: string,
|
|
309
454
|
patterns: readonly RegExp[],
|
|
@@ -340,17 +485,29 @@ export function classifyQuestionIntentFast(
|
|
|
340
485
|
): QuestionIntentClassification {
|
|
341
486
|
const q = question.toLowerCase().trim();
|
|
342
487
|
|
|
488
|
+
if (EXPLICIT_METADATA_OVERRIDE_PATTERNS.some((p) => p.test(q))) {
|
|
489
|
+
return {
|
|
490
|
+
intent: LegendAIQuestionIntent.METADATA,
|
|
491
|
+
metaScore: 1,
|
|
492
|
+
dataScore: 0,
|
|
493
|
+
ambiguous: false,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
343
497
|
const metaScore = countPatternMatches(q, METADATA_SIGNAL_PATTERNS);
|
|
344
498
|
const dataScore = countPatternMatches(q, DATA_QUERY_SIGNAL_PATTERNS);
|
|
345
499
|
|
|
346
500
|
if (metaScore > 0 && dataScore === 0) {
|
|
347
501
|
const isStructural =
|
|
348
502
|
PRODUCT_REFERENCE_PATTERN.test(q) || STRUCTURAL_KEYWORD_PATTERN.test(q);
|
|
503
|
+
const isCapabilityDiscovery = CAPABILITY_DISCOVERY_PATTERNS.some((p) =>
|
|
504
|
+
p.test(q),
|
|
505
|
+
);
|
|
349
506
|
return {
|
|
350
507
|
intent: LegendAIQuestionIntent.METADATA,
|
|
351
508
|
metaScore,
|
|
352
509
|
dataScore,
|
|
353
|
-
ambiguous: hasServices && !isStructural,
|
|
510
|
+
ambiguous: hasServices && !isStructural && !isCapabilityDiscovery,
|
|
354
511
|
};
|
|
355
512
|
}
|
|
356
513
|
if (dataScore > 0 && metaScore === 0) {
|
|
@@ -415,6 +572,45 @@ export function classifyQuestionIntent(
|
|
|
415
572
|
return classifyQuestionIntentFast(question, hasServices).intent;
|
|
416
573
|
}
|
|
417
574
|
|
|
575
|
+
export async function extractParameterSchemas(
|
|
576
|
+
query: string,
|
|
577
|
+
graphManager: AbstractPureGraphManager,
|
|
578
|
+
graphManagerState: GraphManagerState,
|
|
579
|
+
): Promise<{
|
|
580
|
+
parameters: string[];
|
|
581
|
+
parameterSchemas: TDSParameterSchema[];
|
|
582
|
+
parameterExtractionFailed: boolean;
|
|
583
|
+
}> {
|
|
584
|
+
try {
|
|
585
|
+
const rawLambda = await graphManager.pureCodeToLambda(query);
|
|
586
|
+
const varExpressions = buildLambdaVariableExpressions(
|
|
587
|
+
rawLambda,
|
|
588
|
+
graphManagerState,
|
|
589
|
+
).filter(filterByType(VariableExpression));
|
|
590
|
+
return {
|
|
591
|
+
parameters: varExpressions.map((v) => v.name),
|
|
592
|
+
parameterSchemas: varExpressions.map((v) => {
|
|
593
|
+
const schema: TDSParameterSchema = { name: v.name };
|
|
594
|
+
const typePath = v.genericType?.ownerReference.value.path;
|
|
595
|
+
if (typePath) {
|
|
596
|
+
schema.type = extractElementNameFromPath(typePath);
|
|
597
|
+
}
|
|
598
|
+
if (v.multiplicity.lowerBound > 0) {
|
|
599
|
+
schema.required = true;
|
|
600
|
+
}
|
|
601
|
+
return schema;
|
|
602
|
+
}),
|
|
603
|
+
parameterExtractionFailed: false,
|
|
604
|
+
};
|
|
605
|
+
} catch {
|
|
606
|
+
return {
|
|
607
|
+
parameters: [],
|
|
608
|
+
parameterSchemas: [],
|
|
609
|
+
parameterExtractionFailed: true,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
418
614
|
export interface LegendAIChatProps {
|
|
419
615
|
services: TDSServiceSchema[];
|
|
420
616
|
coordinates: string;
|
|
@@ -433,4 +629,16 @@ export interface LegendAIChatProps {
|
|
|
433
629
|
* returned by the orchestrator. Required for the execute-after-generate flow.
|
|
434
630
|
*/
|
|
435
631
|
pureExecutionContext?: QueryExplicitExecutionContextInfo;
|
|
632
|
+
availableScopes?: LegendAIScopeItem[];
|
|
633
|
+
/** Called when the user clicks the close button in the chat header. */
|
|
634
|
+
onClose?: () => void;
|
|
635
|
+
/** Called when the user clicks the minimize button in the chat header. */
|
|
636
|
+
onMinimize?: () => void;
|
|
637
|
+
/**
|
|
638
|
+
* Optional callback fired when users submit thumbs-up/down feedback
|
|
639
|
+
* for an assistant response.
|
|
640
|
+
*/
|
|
641
|
+
onMessageFeedback?: (
|
|
642
|
+
feedback: LegendAIMessageFeedback,
|
|
643
|
+
) => Promise<void> | void;
|
|
436
644
|
}
|
|
@@ -171,6 +171,7 @@ export abstract class LegendAI_LegendApplicationPlugin_Extension extends LegendA
|
|
|
171
171
|
services: TDSServiceSchema[],
|
|
172
172
|
coordinates: string,
|
|
173
173
|
history?: LegendAIConversationTurn[],
|
|
174
|
+
metadata?: LegendAIProductMetadata,
|
|
174
175
|
): string;
|
|
175
176
|
|
|
176
177
|
/**
|
|
@@ -184,8 +185,32 @@ export abstract class LegendAI_LegendApplicationPlugin_Extension extends LegendA
|
|
|
184
185
|
history?: LegendAIConversationTurn[],
|
|
185
186
|
): string;
|
|
186
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Build the LLM prompt for generating a SQL query targeting data product
|
|
190
|
+
* access points. Unlike service-based prompts this uses `p()` syntax,
|
|
191
|
+
* has no coordinates or parameters, and focuses on lakehouse execution.
|
|
192
|
+
*/
|
|
193
|
+
abstract buildAccessPointGeneratorPrompt(
|
|
194
|
+
question: string,
|
|
195
|
+
accessPoints: TDSServiceSchema[],
|
|
196
|
+
history?: LegendAIConversationTurn[],
|
|
197
|
+
): string;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Build the LLM prompt for verifying and correcting a SQL query that
|
|
201
|
+
* targets data product access points using `p()` syntax.
|
|
202
|
+
*/
|
|
203
|
+
abstract buildAccessPointJudgePrompt(
|
|
204
|
+
sql: string,
|
|
205
|
+
question: string,
|
|
206
|
+
accessPoints: TDSServiceSchema[],
|
|
207
|
+
history?: LegendAIConversationTurn[],
|
|
208
|
+
): string;
|
|
209
|
+
|
|
187
210
|
/**
|
|
188
211
|
* Send a prompt to the LLM service and return the raw response text.
|
|
212
|
+
* The plugin manages conversation lifecycle internally — callers
|
|
213
|
+
* do not need to create or track conversations.
|
|
189
214
|
*/
|
|
190
215
|
abstract callLLM(prompt: string, config: LegendAIConfig): Promise<string>;
|
|
191
216
|
|
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { createMock } from '@finos/legend-shared/test';
|
|
18
|
-
import type {
|
|
18
|
+
import type {
|
|
19
|
+
MessageSetter,
|
|
20
|
+
LegendAIOperationContext,
|
|
21
|
+
} from '../stores/LegendAIChatProcessors.js';
|
|
19
22
|
import {
|
|
20
23
|
type LegendAIMessage,
|
|
21
24
|
type LegendAIAssistantMessage,
|
|
@@ -31,6 +34,9 @@ import {
|
|
|
31
34
|
type LegendAISqlExtractionResult,
|
|
32
35
|
type LegendAIJudgeResult,
|
|
33
36
|
LegendAIJudgeVerdict,
|
|
37
|
+
LegendAIOrchestratorResponse,
|
|
38
|
+
LegendAIResolvedEntities,
|
|
39
|
+
LegendAISqlExecutionResultData,
|
|
34
40
|
} from '../LegendAI_LegendApplicationPlugin_Extension.js';
|
|
35
41
|
|
|
36
42
|
export const TEST__createMockSetter = (): {
|
|
@@ -121,6 +127,7 @@ export const TEST__createMockLegendAIPlugin = (
|
|
|
121
127
|
_s: TDSServiceSchema[],
|
|
122
128
|
_c: string,
|
|
123
129
|
_h?: LegendAIConversationTurn[],
|
|
130
|
+
_m?: LegendAIProductMetadata,
|
|
124
131
|
) => 'generator prompt',
|
|
125
132
|
buildJudgePrompt: (
|
|
126
133
|
_sql: string,
|
|
@@ -129,6 +136,17 @@ export const TEST__createMockLegendAIPlugin = (
|
|
|
129
136
|
_c: string,
|
|
130
137
|
_h?: LegendAIConversationTurn[],
|
|
131
138
|
) => 'judge prompt',
|
|
139
|
+
buildAccessPointGeneratorPrompt: (
|
|
140
|
+
_q: string,
|
|
141
|
+
_s: TDSServiceSchema[],
|
|
142
|
+
_h?: LegendAIConversationTurn[],
|
|
143
|
+
) => 'ap generator prompt',
|
|
144
|
+
buildAccessPointJudgePrompt: (
|
|
145
|
+
_sql: string,
|
|
146
|
+
_q: string,
|
|
147
|
+
_s: TDSServiceSchema[],
|
|
148
|
+
_h?: LegendAIConversationTurn[],
|
|
149
|
+
) => 'ap judge prompt',
|
|
132
150
|
callLLM: createMock(),
|
|
133
151
|
executeSql: createMock(),
|
|
134
152
|
extractSqlFromResponse: (_a: string): LegendAISqlExtractionResult => ({
|
|
@@ -142,7 +160,43 @@ export const TEST__createMockLegendAIPlugin = (
|
|
|
142
160
|
_q: string,
|
|
143
161
|
services: TDSServiceSchema[],
|
|
144
162
|
): Promise<TDSServiceSchema[]> => Promise.resolve(services),
|
|
163
|
+
resolveEntitiesForQuery: (): Promise<LegendAIResolvedEntities> => {
|
|
164
|
+
const entities = new LegendAIResolvedEntities();
|
|
165
|
+
entities.rootEntity = 'my::Root';
|
|
166
|
+
entities.relatedEntities = [];
|
|
167
|
+
return Promise.resolve(entities);
|
|
168
|
+
},
|
|
169
|
+
generateQueryViaOrchestrator: (): Promise<LegendAIOrchestratorResponse> => {
|
|
170
|
+
const response = new LegendAIOrchestratorResponse();
|
|
171
|
+
response.legend_query = "model::Entity.all()->project([x|x.id], ['Id'])";
|
|
172
|
+
return Promise.resolve(response);
|
|
173
|
+
},
|
|
174
|
+
executePureQuery: (): Promise<LegendAISqlExecutionResultData> => {
|
|
175
|
+
const data = new LegendAISqlExecutionResultData();
|
|
176
|
+
data.columns = ['Id'];
|
|
177
|
+
data.rows = [{ Id: '1' }];
|
|
178
|
+
return Promise.resolve(data);
|
|
179
|
+
},
|
|
180
|
+
disambiguateEntity: (): Promise<LegendAIResolvedEntities> => {
|
|
181
|
+
const entities = new LegendAIResolvedEntities();
|
|
182
|
+
entities.rootEntity = 'my::Root';
|
|
183
|
+
entities.relatedEntities = [];
|
|
184
|
+
return Promise.resolve(entities);
|
|
185
|
+
},
|
|
145
186
|
buildErrorCorrectionPrompt: (): string => '',
|
|
146
187
|
buildZeroRowCorrectionPrompt: (): string => '',
|
|
147
188
|
...overrides,
|
|
148
189
|
}) as LegendAI_LegendApplicationPlugin_Extension;
|
|
190
|
+
|
|
191
|
+
export const TEST__createOperationContext = (
|
|
192
|
+
overrides?: Partial<LegendAIOperationContext>,
|
|
193
|
+
): LegendAIOperationContext => {
|
|
194
|
+
const { setter } = TEST__createMockSetter();
|
|
195
|
+
return {
|
|
196
|
+
config: TEST_DATA__legendAIConfig,
|
|
197
|
+
plugin: TEST__createMockLegendAIPlugin(),
|
|
198
|
+
history: [],
|
|
199
|
+
setMessages: setter,
|
|
200
|
+
...overrides,
|
|
201
|
+
};
|
|
202
|
+
};
|
|
@@ -17,12 +17,7 @@
|
|
|
17
17
|
import { useMemo } from 'react';
|
|
18
18
|
import { LegendAIChartType } from '../LegendAI_LegendApplicationPlugin_Extension.js';
|
|
19
19
|
import type { LegendAIGridData } from '../LegendAITypes.js';
|
|
20
|
-
import {
|
|
21
|
-
computeKeyMetrics,
|
|
22
|
-
computeChartData,
|
|
23
|
-
inferChartType,
|
|
24
|
-
findNumericColumnName,
|
|
25
|
-
} from './LegendAIAnalysisUtils.js';
|
|
20
|
+
import { analyzeGridData } from './LegendAIAnalysisUtils.js';
|
|
26
21
|
import { LegendAIBarChart, LegendAIDonutChart } from './LegendAICharts.js';
|
|
27
22
|
|
|
28
23
|
export const LegendAIAnalysisPanel = (props: {
|
|
@@ -32,35 +27,20 @@ export const LegendAIAnalysisPanel = (props: {
|
|
|
32
27
|
}): React.ReactNode => {
|
|
33
28
|
const { gridData, summary, SummaryRenderer } = props;
|
|
34
29
|
|
|
35
|
-
const metrics = useMemo(
|
|
36
|
-
|
|
37
|
-
const chartType = useMemo(() => inferChartType(gridData), [gridData]);
|
|
38
|
-
|
|
39
|
-
const chartData = useMemo(
|
|
40
|
-
() =>
|
|
41
|
-
chartType === LegendAIChartType.NONE ? [] : computeChartData(gridData),
|
|
42
|
-
[gridData, chartType],
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
const numericColName = useMemo(
|
|
46
|
-
() => findNumericColumnName(gridData),
|
|
30
|
+
const { metrics, chartType, chartData, numericColumnName } = useMemo(
|
|
31
|
+
() => analyzeGridData(gridData),
|
|
47
32
|
[gridData],
|
|
48
33
|
);
|
|
49
34
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
numericColName === undefined
|
|
60
|
-
? {}
|
|
61
|
-
: { title: `Top ${chartData.length} by ${numericColName}` },
|
|
62
|
-
[numericColName, chartData.length],
|
|
63
|
-
);
|
|
35
|
+
let chartTitle: string | undefined;
|
|
36
|
+
if (numericColumnName !== undefined) {
|
|
37
|
+
chartTitle =
|
|
38
|
+
chartType === LegendAIChartType.PIE
|
|
39
|
+
? `${numericColumnName} Distribution`
|
|
40
|
+
: `Top ${chartData.length} by ${numericColumnName}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const chartTitleProp = chartTitle === undefined ? {} : { title: chartTitle };
|
|
64
44
|
|
|
65
45
|
return (
|
|
66
46
|
<div className="legend-ai-analysis">
|
|
@@ -87,9 +67,9 @@ export const LegendAIAnalysisPanel = (props: {
|
|
|
87
67
|
{chartData.length > 0 && (
|
|
88
68
|
<div className="legend-ai-analysis__chart-section">
|
|
89
69
|
{chartType === LegendAIChartType.PIE ? (
|
|
90
|
-
<LegendAIDonutChart data={chartData} {...
|
|
70
|
+
<LegendAIDonutChart data={chartData} {...chartTitleProp} />
|
|
91
71
|
) : (
|
|
92
|
-
<LegendAIBarChart data={chartData} {...
|
|
72
|
+
<LegendAIBarChart data={chartData} {...chartTitleProp} />
|
|
93
73
|
)}
|
|
94
74
|
</div>
|
|
95
75
|
)}
|