@finos/legend-lego 2.0.217 → 2.0.218

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 (50) 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 +14 -0
  4. package/lib/legend-ai/LegendAIDocEnrichment.d.ts.map +1 -1
  5. package/lib/legend-ai/LegendAIDocEnrichment.js +140 -17
  6. package/lib/legend-ai/LegendAIDocEnrichment.js.map +1 -1
  7. package/lib/legend-ai/LegendAITypes.d.ts +30 -1
  8. package/lib/legend-ai/LegendAITypes.d.ts.map +1 -1
  9. package/lib/legend-ai/LegendAITypes.js +31 -0
  10. package/lib/legend-ai/LegendAITypes.js.map +1 -1
  11. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts +2 -2
  12. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts.map +1 -1
  13. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js +1 -1
  14. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js.map +1 -1
  15. package/lib/legend-ai/components/LegendAIChat.d.ts.map +1 -1
  16. package/lib/legend-ai/components/LegendAIChat.js +62 -38
  17. package/lib/legend-ai/components/LegendAIChat.js.map +1 -1
  18. package/lib/legend-ai/components/LegendAIChatHelpers.d.ts +5 -0
  19. package/lib/legend-ai/components/LegendAIChatHelpers.d.ts.map +1 -1
  20. package/lib/legend-ai/components/LegendAIChatHelpers.js +8 -1
  21. package/lib/legend-ai/components/LegendAIChatHelpers.js.map +1 -1
  22. package/lib/legend-ai/index.d.ts +3 -2
  23. package/lib/legend-ai/index.d.ts.map +1 -1
  24. package/lib/legend-ai/index.js +3 -2
  25. package/lib/legend-ai/index.js.map +1 -1
  26. package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts +27 -2
  27. package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts.map +1 -1
  28. package/lib/legend-ai/stores/LegendAIChatProcessors.js +329 -86
  29. package/lib/legend-ai/stores/LegendAIChatProcessors.js.map +1 -1
  30. package/lib/legend-ai/stores/LegendAIJoinAnalysis.d.ts +11 -0
  31. package/lib/legend-ai/stores/LegendAIJoinAnalysis.d.ts.map +1 -1
  32. package/lib/legend-ai/stores/LegendAIJoinAnalysis.js +53 -19
  33. package/lib/legend-ai/stores/LegendAIJoinAnalysis.js.map +1 -1
  34. package/lib/legend-ai/stores/LegendAISqlHelpers.d.ts +4 -1
  35. package/lib/legend-ai/stores/LegendAISqlHelpers.d.ts.map +1 -1
  36. package/lib/legend-ai/stores/LegendAISqlHelpers.js +25 -2
  37. package/lib/legend-ai/stores/LegendAISqlHelpers.js.map +1 -1
  38. package/lib/legend-ai/stores/LegendAISqlJoinSanitizers.js +4 -4
  39. package/lib/legend-ai/stores/LegendAISqlJoinSanitizers.js.map +1 -1
  40. package/package.json +3 -3
  41. package/src/legend-ai/LegendAIDocEnrichment.ts +189 -23
  42. package/src/legend-ai/LegendAITypes.ts +54 -1
  43. package/src/legend-ai/LegendAI_LegendApplicationPlugin_Extension.ts +2 -0
  44. package/src/legend-ai/components/LegendAIChat.tsx +114 -78
  45. package/src/legend-ai/components/LegendAIChatHelpers.ts +9 -3
  46. package/src/legend-ai/index.ts +7 -0
  47. package/src/legend-ai/stores/LegendAIChatProcessors.ts +476 -105
  48. package/src/legend-ai/stores/LegendAIJoinAnalysis.ts +82 -19
  49. package/src/legend-ai/stores/LegendAISqlHelpers.ts +32 -2
  50. package/src/legend-ai/stores/LegendAISqlJoinSanitizers.ts +4 -4
@@ -56,6 +56,7 @@ import {
56
56
  LegendAIThinkingStepStatus,
57
57
  LegendAIMessageRole,
58
58
  LegendAIErrorType,
59
+ LegendAIPythonCodeStatus,
59
60
  TDSServiceSourceType,
60
61
  LEGEND_AI_FEEDBACK_PROMPT,
61
62
  classifyQuestionIntentFast,
@@ -65,6 +66,7 @@ import {
65
66
  } from '../LegendAITypes.js';
66
67
  import { useLegendAIChatState } from '../stores/LegendAIChatState.js';
67
68
  import { looksLikeAccessError } from '../stores/LegendAIChatProcessors.js';
69
+ import { accessPointName } from '../stores/LegendAISqlHelpers.js';
68
70
  import { LegendAIResultGrid } from './LegendAIResultGrid.js';
69
71
  import { LegendAIAnalysisPanel } from './LegendAIAnalysisPanel.js';
70
72
  import { LegendAIChatInput } from './LegendAIChatInput.js';
@@ -76,9 +78,12 @@ const COPY_FEEDBACK_DURATION_MS = 2000;
76
78
  const CONTEXT_BANNER_AUTO_DISMISS_MS = 20000;
77
79
 
78
80
  type LegendAIPythonCodeEntry =
79
- | { status: 'loading' }
80
- | { status: 'ready'; code: LegendAIPythonQueryCode | undefined }
81
- | { status: 'error'; errorMessage: string };
81
+ | { status: LegendAIPythonCodeStatus.LOADING }
82
+ | {
83
+ status: LegendAIPythonCodeStatus.READY;
84
+ code: LegendAIPythonQueryCode | undefined;
85
+ }
86
+ | { status: LegendAIPythonCodeStatus.ERROR; errorMessage: string };
82
87
 
83
88
  const LegendAIContextBanner = (props: {
84
89
  message: string;
@@ -328,37 +333,48 @@ const AssistantMessageView = memo(function AssistantMessageView(props: {
328
333
 
329
334
  const handleCopySql = useCallback(() => {
330
335
  if (msg.sql) {
331
- navigator.clipboard.writeText(msg.sql).catch(noop);
332
- setSqlCopied(true);
336
+ navigator.clipboard
337
+ .writeText(msg.sql)
338
+ .then(() => {
339
+ setSqlCopied(true);
340
+ if (copyTimerRef.current !== undefined) {
341
+ clearTimeout(copyTimerRef.current);
342
+ }
343
+ copyTimerRef.current = setTimeout(() => {
344
+ setSqlCopied(false);
345
+ copyTimerRef.current = undefined;
346
+ }, COPY_FEEDBACK_DURATION_MS);
347
+ })
348
+ .catch(noop());
333
349
  onLogTelemetryEvent?.({
334
350
  type: LegendAIChatTelemetryEventType.ARTIFACT_COPIED,
335
351
  artifact: LegendAITelemetryArtifact.SQL,
336
352
  });
337
- if (copyTimerRef.current !== undefined) {
338
- clearTimeout(copyTimerRef.current);
339
- }
340
- copyTimerRef.current = setTimeout(() => {
341
- setSqlCopied(false);
342
- copyTimerRef.current = undefined;
343
- }, COPY_FEEDBACK_DURATION_MS);
344
353
  }
345
354
  }, [msg.sql, onLogTelemetryEvent]);
346
355
 
347
356
  const handleCopyPython = useCallback(() => {
348
- if (pythonEntry?.status === 'ready' && pythonEntry.code) {
349
- navigator.clipboard.writeText(pythonEntry.code.code).catch(noop);
350
- setPythonCopied(true);
357
+ if (
358
+ pythonEntry?.status === LegendAIPythonCodeStatus.READY &&
359
+ pythonEntry.code
360
+ ) {
361
+ navigator.clipboard
362
+ .writeText(pythonEntry.code.code)
363
+ .then(() => {
364
+ setPythonCopied(true);
365
+ if (pythonCopyTimerRef.current !== undefined) {
366
+ clearTimeout(pythonCopyTimerRef.current);
367
+ }
368
+ pythonCopyTimerRef.current = setTimeout(() => {
369
+ setPythonCopied(false);
370
+ pythonCopyTimerRef.current = undefined;
371
+ }, COPY_FEEDBACK_DURATION_MS);
372
+ })
373
+ .catch(noop());
351
374
  onLogTelemetryEvent?.({
352
375
  type: LegendAIChatTelemetryEventType.ARTIFACT_COPIED,
353
376
  artifact: LegendAITelemetryArtifact.PYTHON,
354
377
  });
355
- if (pythonCopyTimerRef.current !== undefined) {
356
- clearTimeout(pythonCopyTimerRef.current);
357
- }
358
- pythonCopyTimerRef.current = setTimeout(() => {
359
- setPythonCopied(false);
360
- pythonCopyTimerRef.current = undefined;
361
- }, COPY_FEEDBACK_DURATION_MS);
362
378
  }
363
379
  }, [pythonEntry, onLogTelemetryEvent]);
364
380
 
@@ -414,7 +430,7 @@ const AssistantMessageView = memo(function AssistantMessageView(props: {
414
430
  : { rowCount: msg.gridData.rowData.length }),
415
431
  });
416
432
  if (result instanceof Promise) {
417
- result.catch(noop);
433
+ result.catch(noop());
418
434
  }
419
435
  },
420
436
  [msg, onMessageFeedback, questionText],
@@ -732,31 +748,32 @@ const AssistantMessageView = memo(function AssistantMessageView(props: {
732
748
  <CodeIcon />
733
749
  </span>
734
750
  <span>Python</span>
735
- {pythonEntry?.status === 'ready' && pythonEntry.code && (
736
- <button
737
- type="button"
738
- className="legend-ai__sql-copy-btn"
739
- title="Copy Python code"
740
- aria-label="Copy Python code"
741
- onClick={handleCopyPython}
742
- >
743
- {pythonCopied ? (
744
- <span className="legend-ai__sql-copy-btn--copied">
745
- <CheckIcon />
746
- </span>
747
- ) : (
748
- <CopyIcon />
749
- )}
750
- </button>
751
- )}
751
+ {pythonEntry?.status === LegendAIPythonCodeStatus.READY &&
752
+ pythonEntry.code && (
753
+ <button
754
+ type="button"
755
+ className="legend-ai__sql-copy-btn"
756
+ title="Copy Python code"
757
+ aria-label="Copy Python code"
758
+ onClick={handleCopyPython}
759
+ >
760
+ {pythonCopied ? (
761
+ <span className="legend-ai__sql-copy-btn--copied">
762
+ <CheckIcon />
763
+ </span>
764
+ ) : (
765
+ <CopyIcon />
766
+ )}
767
+ </button>
768
+ )}
752
769
  </div>
753
- {pythonEntry?.status === 'loading' && (
770
+ {pythonEntry?.status === LegendAIPythonCodeStatus.LOADING && (
754
771
  <div className="legend-ai__python-panel-loading">
755
772
  <LoadingIcon isLoading={true} />
756
773
  <span>Generating Python…</span>
757
774
  </div>
758
775
  )}
759
- {pythonEntry?.status === 'error' && (
776
+ {pythonEntry?.status === LegendAIPythonCodeStatus.ERROR && (
760
777
  <div className="legend-ai__python-panel-error">
761
778
  <span>
762
779
  Could not generate Python code. Try again in a moment.
@@ -775,7 +792,7 @@ const AssistantMessageView = memo(function AssistantMessageView(props: {
775
792
  </button>
776
793
  </div>
777
794
  )}
778
- {pythonEntry?.status === 'ready' &&
795
+ {pythonEntry?.status === LegendAIPythonCodeStatus.READY &&
779
796
  pythonEntry.code === undefined && (
780
797
  <div className="legend-ai__python-panel-loading">
781
798
  <span>
@@ -783,28 +800,29 @@ const AssistantMessageView = memo(function AssistantMessageView(props: {
783
800
  </span>
784
801
  </div>
785
802
  )}
786
- {pythonEntry?.status === 'ready' && pythonEntry.code && (
787
- <>
788
- <div className="legend-ai__sql-scroll">
789
- <pre className="legend-ai__sql-display">
790
- {pythonEntry.code.code}
791
- </pre>
792
- </div>
793
- {pythonEntry.code.notebookUrl && (
794
- <div className="legend-ai__python-panel-actions">
795
- <a
796
- className="legend-ai__permission-error-btn"
797
- href={pythonEntry.code.notebookUrl}
798
- target="_blank"
799
- rel="noopener noreferrer"
800
- >
801
- <JupyterIcon />
802
- <span>Launch Notebook</span>
803
- </a>
803
+ {pythonEntry?.status === LegendAIPythonCodeStatus.READY &&
804
+ pythonEntry.code && (
805
+ <>
806
+ <div className="legend-ai__sql-scroll">
807
+ <pre className="legend-ai__sql-display">
808
+ {pythonEntry.code.code}
809
+ </pre>
804
810
  </div>
805
- )}
806
- </>
807
- )}
811
+ {pythonEntry.code.notebookUrl && (
812
+ <div className="legend-ai__python-panel-actions">
813
+ <a
814
+ className="legend-ai__permission-error-btn"
815
+ href={pythonEntry.code.notebookUrl}
816
+ target="_blank"
817
+ rel="noopener noreferrer"
818
+ >
819
+ <JupyterIcon />
820
+ <span>Launch Notebook</span>
821
+ </a>
822
+ </div>
823
+ )}
824
+ </>
825
+ )}
808
826
  </div>
809
827
  )}
810
828
  </div>
@@ -933,25 +951,40 @@ export const LegendAIChat = (props: LegendAIChatProps): React.ReactNode => {
933
951
  >(new Map());
934
952
  const pythonCodeByMessageIdRef = useRef(pythonCodeByMessageId);
935
953
  pythonCodeByMessageIdRef.current = pythonCodeByMessageId;
954
+ /**
955
+ * Generates Python for the access point the answer actually queried. When the
956
+ * message names one that is not loaded, no code is offered rather than a guess.
957
+ */
936
958
  const requestPythonCode = useCallback(
937
959
  async (msg: LegendAIAssistantMessage): Promise<void> => {
938
960
  const existing = pythonCodeByMessageIdRef.current.get(msg.id);
939
- if (existing?.status === 'loading' || existing?.status === 'ready') {
961
+ if (
962
+ existing?.status === LegendAIPythonCodeStatus.LOADING ||
963
+ existing?.status === LegendAIPythonCodeStatus.READY
964
+ ) {
940
965
  return;
941
966
  }
942
967
  setPythonCodeByMessageId((prev) => {
943
968
  const next = new Map(prev);
944
- next.set(msg.id, { status: 'loading' });
969
+ next.set(msg.id, { status: LegendAIPythonCodeStatus.LOADING });
945
970
  return next;
946
971
  });
947
- const resolvedSet = new Set(msg.queriedAccessPoints);
972
+ const resolvedSet = new Set(
973
+ msg.queriedAccessPoints.map((name) => name.toLowerCase()),
974
+ );
948
975
  const service =
949
- services.find((s) => resolvedSet.has(s.pattern.replace(/^\//u, ''))) ??
950
- services[0];
976
+ resolvedSet.size > 0
977
+ ? services.find((s) =>
978
+ resolvedSet.has(accessPointName(s).toLowerCase()),
979
+ )
980
+ : services[0];
951
981
  if (!service) {
952
982
  setPythonCodeByMessageId((prev) => {
953
983
  const next = new Map(prev);
954
- next.set(msg.id, { status: 'ready', code: undefined });
984
+ next.set(msg.id, {
985
+ status: LegendAIPythonCodeStatus.READY,
986
+ code: undefined,
987
+ });
955
988
  return next;
956
989
  });
957
990
  return;
@@ -974,14 +1007,17 @@ export const LegendAIChat = (props: LegendAIChatProps): React.ReactNode => {
974
1007
  const code = await plugin.generatePythonQueryCodeAsync(request);
975
1008
  setPythonCodeByMessageId((prev) => {
976
1009
  const next = new Map(prev);
977
- next.set(msg.id, { status: 'ready', code });
1010
+ next.set(msg.id, { status: LegendAIPythonCodeStatus.READY, code });
978
1011
  return next;
979
1012
  });
980
1013
  } catch (error) {
981
1014
  assertErrorThrown(error);
982
1015
  setPythonCodeByMessageId((prev) => {
983
1016
  const next = new Map(prev);
984
- next.set(msg.id, { status: 'error', errorMessage: error.message });
1017
+ next.set(msg.id, {
1018
+ status: LegendAIPythonCodeStatus.ERROR,
1019
+ errorMessage: error.message,
1020
+ });
985
1021
  return next;
986
1022
  });
987
1023
  }
@@ -999,13 +1035,13 @@ export const LegendAIChat = (props: LegendAIChatProps): React.ReactNode => {
999
1035
  if (!onOpenInDataCube || msg.sql === null) {
1000
1036
  return;
1001
1037
  }
1002
- const accessPointName = msg.queriedAccessPoints[0];
1003
- if (accessPointName === undefined) {
1038
+ const apName = msg.queriedAccessPoints[0];
1039
+ if (apName === undefined) {
1004
1040
  return;
1005
1041
  }
1006
1042
  const service =
1007
1043
  services.find(
1008
- (s) => s.pattern.replace(/^\//u, '') === accessPointName,
1044
+ (s) => accessPointName(s).toLowerCase() === apName.toLowerCase(),
1009
1045
  ) ?? services[0];
1010
1046
  if (!service) {
1011
1047
  return;
@@ -1038,7 +1074,7 @@ export const LegendAIChat = (props: LegendAIChatProps): React.ReactNode => {
1038
1074
  assertErrorThrown(error);
1039
1075
  }
1040
1076
  try {
1041
- onOpenInDataCube(accessPointName, pureQuery);
1077
+ onOpenInDataCube(apName, pureQuery);
1042
1078
  } finally {
1043
1079
  setOpeningDataCubeMessageIds((prev) => {
1044
1080
  if (!prev.has(msg.id)) {
@@ -1119,13 +1155,13 @@ export const LegendAIChat = (props: LegendAIChatProps): React.ReactNode => {
1119
1155
  );
1120
1156
  const handleRequestPython = useCallback(
1121
1157
  (message: LegendAIAssistantMessage): void => {
1122
- requestPythonCode(message).catch(noop);
1158
+ requestPythonCode(message).catch(noop());
1123
1159
  },
1124
1160
  [requestPythonCode],
1125
1161
  );
1126
1162
  const handleOpenInDataCube = useCallback(
1127
1163
  (message: LegendAIAssistantMessage): void => {
1128
- handleOpenInDataCubeMsg(message).catch(noop);
1164
+ handleOpenInDataCubeMsg(message).catch(noop());
1129
1165
  },
1130
1166
  [handleOpenInDataCubeMsg],
1131
1167
  );
@@ -69,10 +69,16 @@ const INFRASTRUCTURE_COLUMN_PATTERN =
69
69
  /^lake_|^__lake|^vlf|^batch|_ap_lh_migration/i;
70
70
  const AP_DATE_NAME_PATTERN = /(?:date|dt|time)$/i;
71
71
 
72
+ /**
73
+ * Whether the column holds text. Unlike {@link isStringColumn} this keeps
74
+ * identifier columns, which are the usual join keys.
75
+ */
76
+ export function isStringTypedColumn(c: TDSColumnSchema): boolean {
77
+ return STRING_TYPE_NAMES.has(c.type ?? '');
78
+ }
79
+
72
80
  export function isStringColumn(c: TDSColumnSchema): boolean {
73
- return (
74
- STRING_TYPE_NAMES.has(c.type ?? '') && !c.name.toLowerCase().includes('id')
75
- );
81
+ return isStringTypedColumn(c) && !c.name.toLowerCase().includes('id');
76
82
  }
77
83
 
78
84
  export function isNumericColumn(c: TDSColumnSchema): boolean {
@@ -58,6 +58,8 @@ export {
58
58
  attachMetadataOverview,
59
59
  elapsedSeconds,
60
60
  createMessagePair,
61
+ createAssistantMessage,
62
+ buildOrchestratorFallbackAction,
61
63
  analyzeOrchestratorResults,
62
64
  cleanLlmSqlResponse,
63
65
  isValidSqlCorrection,
@@ -91,6 +93,10 @@ export {
91
93
  sanitizeJoinSameKeyColumns,
92
94
  wrapBareJoinAccessPoints,
93
95
  } from './stores/LegendAISqlJoinSanitizers.js';
96
+ export {
97
+ accessPointName,
98
+ sharedColumnNames,
99
+ } from './stores/LegendAISqlHelpers.js';
94
100
  export {
95
101
  preFilterServicesByRelevance,
96
102
  isFuzzyMatch,
@@ -117,6 +123,7 @@ export {
117
123
  enrichColumnsFromElementDocs,
118
124
  inferServiceRelationshipsFromAssociations,
119
125
  extractLambdaPreFilters,
126
+ extractServicePreFilters,
120
127
  extractModelContext,
121
128
  buildEnrichedBusinessContext,
122
129
  buildModelContextEnrichmentText,