@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.
Files changed (67) 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 +60 -0
  4. package/lib/legend-ai/LegendAIDocEnrichment.d.ts.map +1 -0
  5. package/lib/legend-ai/LegendAIDocEnrichment.js +429 -0
  6. package/lib/legend-ai/LegendAIDocEnrichment.js.map +1 -0
  7. package/lib/legend-ai/LegendAITypes.d.ts +127 -1
  8. package/lib/legend-ai/LegendAITypes.d.ts.map +1 -1
  9. package/lib/legend-ai/LegendAITypes.js +111 -2
  10. package/lib/legend-ai/LegendAITypes.js.map +1 -1
  11. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts +14 -1
  12. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts.map +1 -1
  13. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js.map +1 -1
  14. package/lib/legend-ai/__test-utils__/LegendAITestUtils.d.ts +2 -1
  15. package/lib/legend-ai/__test-utils__/LegendAITestUtils.d.ts.map +1 -1
  16. package/lib/legend-ai/__test-utils__/LegendAITestUtils.js +37 -2
  17. package/lib/legend-ai/__test-utils__/LegendAITestUtils.js.map +1 -1
  18. package/lib/legend-ai/components/LegendAIAnalysisPanel.d.ts.map +1 -1
  19. package/lib/legend-ai/components/LegendAIAnalysisPanel.js +11 -12
  20. package/lib/legend-ai/components/LegendAIAnalysisPanel.js.map +1 -1
  21. package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts +7 -0
  22. package/lib/legend-ai/components/LegendAIAnalysisUtils.d.ts.map +1 -1
  23. package/lib/legend-ai/components/LegendAIAnalysisUtils.js +106 -41
  24. package/lib/legend-ai/components/LegendAIAnalysisUtils.js.map +1 -1
  25. package/lib/legend-ai/components/LegendAIChat.d.ts +1 -5
  26. package/lib/legend-ai/components/LegendAIChat.d.ts.map +1 -1
  27. package/lib/legend-ai/components/LegendAIChat.js +168 -109
  28. package/lib/legend-ai/components/LegendAIChat.js.map +1 -1
  29. package/lib/legend-ai/components/LegendAIChatHelpers.d.ts +21 -0
  30. package/lib/legend-ai/components/LegendAIChatHelpers.d.ts.map +1 -0
  31. package/lib/legend-ai/components/LegendAIChatHelpers.js +85 -0
  32. package/lib/legend-ai/components/LegendAIChatHelpers.js.map +1 -0
  33. package/lib/legend-ai/components/LegendAIChatInput.d.ts +21 -0
  34. package/lib/legend-ai/components/LegendAIChatInput.d.ts.map +1 -0
  35. package/lib/legend-ai/components/LegendAIChatInput.js +78 -0
  36. package/lib/legend-ai/components/LegendAIChatInput.js.map +1 -0
  37. package/lib/legend-ai/components/LegendAIScopeSelector.d.ts +25 -0
  38. package/lib/legend-ai/components/LegendAIScopeSelector.d.ts.map +1 -0
  39. package/lib/legend-ai/components/LegendAIScopeSelector.js +85 -0
  40. package/lib/legend-ai/components/LegendAIScopeSelector.js.map +1 -0
  41. package/lib/legend-ai/index.d.ts +8 -3
  42. package/lib/legend-ai/index.d.ts.map +1 -1
  43. package/lib/legend-ai/index.js +8 -3
  44. package/lib/legend-ai/index.js.map +1 -1
  45. package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts +105 -0
  46. package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts.map +1 -0
  47. package/lib/legend-ai/stores/LegendAIChatProcessors.js +1482 -0
  48. package/lib/legend-ai/stores/LegendAIChatProcessors.js.map +1 -0
  49. package/lib/legend-ai/stores/LegendAIChatState.d.ts +2 -35
  50. package/lib/legend-ai/stores/LegendAIChatState.d.ts.map +1 -1
  51. package/lib/legend-ai/stores/LegendAIChatState.js +114 -949
  52. package/lib/legend-ai/stores/LegendAIChatState.js.map +1 -1
  53. package/package.json +5 -5
  54. package/src/legend-ai/LegendAIDocEnrichment.ts +572 -0
  55. package/src/legend-ai/LegendAITypes.ts +213 -5
  56. package/src/legend-ai/LegendAI_LegendApplicationPlugin_Extension.ts +25 -0
  57. package/src/legend-ai/__test-utils__/LegendAITestUtils.ts +55 -1
  58. package/src/legend-ai/components/LegendAIAnalysisPanel.tsx +14 -34
  59. package/src/legend-ai/components/LegendAIAnalysisUtils.ts +157 -47
  60. package/src/legend-ai/components/LegendAIChat.tsx +389 -206
  61. package/src/legend-ai/components/LegendAIChatHelpers.ts +117 -0
  62. package/src/legend-ai/components/LegendAIChatInput.tsx +209 -0
  63. package/src/legend-ai/components/LegendAIScopeSelector.tsx +199 -0
  64. package/src/legend-ai/index.ts +31 -4
  65. package/src/legend-ai/stores/LegendAIChatProcessors.ts +2563 -0
  66. package/src/legend-ai/stores/LegendAIChatState.ts +161 -1697
  67. 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 type {
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|join|aggregate)\b/,
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 { MessageSetter } from '../stores/LegendAIChatState.js';
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(() => computeKeyMetrics(gridData), [gridData]);
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
- const donutTitleProp = useMemo(
51
- () =>
52
- numericColName === undefined
53
- ? {}
54
- : { title: `${numericColName} Distribution` },
55
- [numericColName],
56
- );
57
- const barTitleProp = useMemo(
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} {...donutTitleProp} />
70
+ <LegendAIDonutChart data={chartData} {...chartTitleProp} />
91
71
  ) : (
92
- <LegendAIBarChart data={chartData} {...barTitleProp} />
72
+ <LegendAIBarChart data={chartData} {...chartTitleProp} />
93
73
  )}
94
74
  </div>
95
75
  )}