@finos/legend-lego 2.0.211 → 2.0.213
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.css +2 -2
- package/lib/index.css.map +1 -1
- package/lib/legend-ai/LegendAIDocEnrichment.d.ts +12 -12
- package/lib/legend-ai/LegendAIDocEnrichment.d.ts.map +1 -1
- package/lib/legend-ai/LegendAIDocEnrichment.js +317 -86
- package/lib/legend-ai/LegendAIDocEnrichment.js.map +1 -1
- package/lib/legend-ai/LegendAIHostIntegration.d.ts +31 -0
- package/lib/legend-ai/LegendAIHostIntegration.d.ts.map +1 -0
- package/lib/legend-ai/LegendAIHostIntegration.js +50 -0
- package/lib/legend-ai/LegendAIHostIntegration.js.map +1 -0
- package/lib/legend-ai/LegendAIServiceRetrieval.d.ts.map +1 -1
- package/lib/legend-ai/LegendAIServiceRetrieval.js +34 -35
- package/lib/legend-ai/LegendAIServiceRetrieval.js.map +1 -1
- package/lib/legend-ai/LegendAITypes.d.ts +85 -7
- package/lib/legend-ai/LegendAITypes.d.ts.map +1 -1
- package/lib/legend-ai/LegendAITypes.js +99 -1
- package/lib/legend-ai/LegendAITypes.js.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts +36 -3
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js +29 -0
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js.map +1 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.d.ts.map +1 -1
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.js +7 -0
- package/lib/legend-ai/__test-utils__/LegendAITestUtils.js.map +1 -1
- package/lib/legend-ai/components/LegendAICharts.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAICharts.js +6 -10
- package/lib/legend-ai/components/LegendAICharts.js.map +1 -1
- package/lib/legend-ai/components/LegendAIChat.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIChat.js +263 -41
- package/lib/legend-ai/components/LegendAIChat.js.map +1 -1
- package/lib/legend-ai/components/LegendAIChatInput.d.ts +1 -1
- package/lib/legend-ai/components/LegendAIChatInput.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIChatInput.js +2 -1
- package/lib/legend-ai/components/LegendAIChatInput.js.map +1 -1
- package/lib/legend-ai/index.d.ts +5 -2
- package/lib/legend-ai/index.d.ts.map +1 -1
- package/lib/legend-ai/index.js +5 -2
- package/lib/legend-ai/index.js.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts +9 -3
- package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatProcessors.js +794 -220
- package/lib/legend-ai/stores/LegendAIChatProcessors.js.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatState.d.ts.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatState.js +47 -21
- package/lib/legend-ai/stores/LegendAIChatState.js.map +1 -1
- package/lib/legend-ai/stores/LegendAIJoinAnalysis.d.ts +29 -0
- package/lib/legend-ai/stores/LegendAIJoinAnalysis.d.ts.map +1 -0
- package/lib/legend-ai/stores/LegendAIJoinAnalysis.js +124 -0
- package/lib/legend-ai/stores/LegendAIJoinAnalysis.js.map +1 -0
- package/lib/legend-ai/stores/LegendAISqlHelpers.d.ts +25 -0
- package/lib/legend-ai/stores/LegendAISqlHelpers.d.ts.map +1 -0
- package/lib/legend-ai/stores/LegendAISqlHelpers.js +64 -0
- package/lib/legend-ai/stores/LegendAISqlHelpers.js.map +1 -0
- package/lib/legend-ai/stores/LegendAISqlJoinSanitizers.d.ts +22 -0
- package/lib/legend-ai/stores/LegendAISqlJoinSanitizers.d.ts.map +1 -0
- package/lib/legend-ai/stores/LegendAISqlJoinSanitizers.js +411 -0
- package/lib/legend-ai/stores/LegendAISqlJoinSanitizers.js.map +1 -0
- package/package.json +6 -6
- package/src/legend-ai/LegendAIDocEnrichment.ts +435 -97
- package/src/legend-ai/LegendAIHostIntegration.tsx +80 -0
- package/src/legend-ai/LegendAIServiceRetrieval.ts +50 -35
- package/src/legend-ai/LegendAITypes.ts +169 -5
- package/src/legend-ai/LegendAI_LegendApplicationPlugin_Extension.ts +73 -1
- package/src/legend-ai/__test-utils__/LegendAITestUtils.ts +10 -0
- package/src/legend-ai/components/LegendAICharts.tsx +30 -33
- package/src/legend-ai/components/LegendAIChat.tsx +555 -91
- package/src/legend-ai/components/LegendAIChatInput.tsx +6 -2
- package/src/legend-ai/index.ts +22 -3
- package/src/legend-ai/stores/LegendAIChatProcessors.ts +1286 -322
- package/src/legend-ai/stores/LegendAIChatState.ts +73 -28
- package/src/legend-ai/stores/LegendAIJoinAnalysis.ts +165 -0
- package/src/legend-ai/stores/LegendAISqlHelpers.ts +82 -0
- package/src/legend-ai/stores/LegendAISqlJoinSanitizers.ts +515 -0
- package/tsconfig.json +4 -0
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
extractElementNameFromPath,
|
|
20
20
|
PRIMITIVE_TYPE,
|
|
21
21
|
} from '@finos/legend-graph';
|
|
22
|
+
import { isNonNullable } from '@finos/legend-shared';
|
|
22
23
|
import {
|
|
23
24
|
ClassDocumentationEntry,
|
|
24
25
|
AssociationDocumentationEntry,
|
|
@@ -40,7 +41,9 @@ import type {
|
|
|
40
41
|
LegendAIEnrichedBusinessContext,
|
|
41
42
|
LegendAIBusinessContextProperty,
|
|
42
43
|
LegendAIAdditionalNlModelContext,
|
|
44
|
+
LegendAIResolvedEntities,
|
|
43
45
|
} from './LegendAI_LegendApplicationPlugin_Extension.js';
|
|
46
|
+
import { sharedColumnNames } from './stores/LegendAISqlHelpers.js';
|
|
44
47
|
|
|
45
48
|
/**
|
|
46
49
|
* Builds a lookup map from lowercase property name to its documentation entry.
|
|
@@ -167,16 +170,6 @@ function buildAssociationAdjacency(
|
|
|
167
170
|
return adjacency;
|
|
168
171
|
}
|
|
169
172
|
|
|
170
|
-
function findSharedColumns(
|
|
171
|
-
svcA: TDSServiceSchema,
|
|
172
|
-
svcB: TDSServiceSchema,
|
|
173
|
-
): string[] {
|
|
174
|
-
const colsA = new Set(svcA.columns.map((c) => c.name.toLowerCase()));
|
|
175
|
-
return svcB.columns
|
|
176
|
-
.filter((c) => colsA.has(c.name.toLowerCase()))
|
|
177
|
-
.map((c) => c.name);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
173
|
function findViaConnectionFromClass(
|
|
181
174
|
clsA: string,
|
|
182
175
|
classNamesB: string[],
|
|
@@ -218,7 +211,7 @@ function findViaRelationship(
|
|
|
218
211
|
return {
|
|
219
212
|
leftService: svcA.title,
|
|
220
213
|
rightService: svcB.title,
|
|
221
|
-
joinColumns:
|
|
214
|
+
joinColumns: sharedColumnNames(svcB, svcA),
|
|
222
215
|
viaEntity: via.viaEntity,
|
|
223
216
|
leftCardinality: via.leftMult,
|
|
224
217
|
rightCardinality: via.rightMult,
|
|
@@ -246,7 +239,7 @@ function findDirectRelationship(
|
|
|
246
239
|
return {
|
|
247
240
|
leftService: svcA.title,
|
|
248
241
|
rightService: svcB.title,
|
|
249
|
-
joinColumns:
|
|
242
|
+
joinColumns: sharedColumnNames(svcB, svcA),
|
|
250
243
|
};
|
|
251
244
|
}
|
|
252
245
|
}
|
|
@@ -549,42 +542,6 @@ export function extractLambdaPreFilters(
|
|
|
549
542
|
return results;
|
|
550
543
|
}
|
|
551
544
|
|
|
552
|
-
/**
|
|
553
|
-
* Formats pre-filter constraints into a human-readable summary for LLM context.
|
|
554
|
-
* This generates a concise description of what's hardcoded in the service lambda
|
|
555
|
-
* so the AI avoids generating contradictory WHERE clauses.
|
|
556
|
-
*/
|
|
557
|
-
export function formatPreFiltersForContext(
|
|
558
|
-
preFilters: TDSServicePreFilter[],
|
|
559
|
-
): string {
|
|
560
|
-
if (preFilters.length === 0) {
|
|
561
|
-
return '';
|
|
562
|
-
}
|
|
563
|
-
const parts: string[] = [];
|
|
564
|
-
for (const pf of preFilters) {
|
|
565
|
-
const shortProp = pf.property.includes('.')
|
|
566
|
-
? (pf.property.split('.').pop() ?? pf.property)
|
|
567
|
-
: pf.property;
|
|
568
|
-
switch (pf.operator) {
|
|
569
|
-
case 'equal':
|
|
570
|
-
parts.push(`${shortProp} = '${String(pf.value)}'`);
|
|
571
|
-
break;
|
|
572
|
-
case 'isEmpty':
|
|
573
|
-
parts.push(`${shortProp} IS NULL (always)`);
|
|
574
|
-
break;
|
|
575
|
-
case 'isNotEmpty':
|
|
576
|
-
parts.push(`${shortProp} IS NOT NULL (always)`);
|
|
577
|
-
break;
|
|
578
|
-
case 'isNotNull':
|
|
579
|
-
parts.push(`${shortProp} IS NOT NULL (post-filter)`);
|
|
580
|
-
break;
|
|
581
|
-
default:
|
|
582
|
-
break;
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
return `Pre-applied filters: ${parts.join('; ')}`;
|
|
586
|
-
}
|
|
587
|
-
|
|
588
545
|
// ────────────────────────────────────────────────────────────────────────────
|
|
589
546
|
// Model context extraction from DataSpace elementDocs
|
|
590
547
|
// ────────────────────────────────────────────────────────────────────────────
|
|
@@ -597,6 +554,8 @@ export function formatPreFiltersForContext(
|
|
|
597
554
|
*
|
|
598
555
|
* Only applicable to DataSpaces (not DataProducts, which lack elementDocs).
|
|
599
556
|
*/
|
|
557
|
+
const UNKNOWN_TYPE = 'Unknown';
|
|
558
|
+
|
|
600
559
|
function collectClassEntities(
|
|
601
560
|
classMap: Map<string, ClassDocumentationEntry>,
|
|
602
561
|
): LegendAIModelEntity[] {
|
|
@@ -605,7 +564,7 @@ function collectClassEntities(
|
|
|
605
564
|
const upperBound = prop.multiplicity?.upperBound;
|
|
606
565
|
return {
|
|
607
566
|
name: prop.name,
|
|
608
|
-
type: prop.type ??
|
|
567
|
+
type: prop.type ?? UNKNOWN_TYPE,
|
|
609
568
|
isCollection: upperBound === undefined || upperBound > 1,
|
|
610
569
|
isOptional: (prop.multiplicity?.lowerBound ?? 0) === 0,
|
|
611
570
|
};
|
|
@@ -939,6 +898,43 @@ function buildCrossClassNlHints(
|
|
|
939
898
|
];
|
|
940
899
|
}
|
|
941
900
|
|
|
901
|
+
const MAX_VALID_VALUES_PER_PROPERTY = 30;
|
|
902
|
+
|
|
903
|
+
function buildValidValueNlHints(
|
|
904
|
+
rootEntity: string,
|
|
905
|
+
relatedEntities: string[],
|
|
906
|
+
modelContext: LegendAIModelContext,
|
|
907
|
+
entityMap: Map<string, LegendAIModelEntity>,
|
|
908
|
+
): LegendAIAdditionalNlModelContext[] {
|
|
909
|
+
const valuesByEnum = new Map<string, string[]>();
|
|
910
|
+
for (const enumeration of modelContext.enumerations ?? []) {
|
|
911
|
+
valuesByEnum.set(enumeration.path, enumeration.values);
|
|
912
|
+
valuesByEnum.set(enumeration.name, enumeration.values);
|
|
913
|
+
}
|
|
914
|
+
if (valuesByEnum.size === 0) {
|
|
915
|
+
return [];
|
|
916
|
+
}
|
|
917
|
+
const entries: LegendAIAdditionalNlModelContext[] = [];
|
|
918
|
+
for (const path of [rootEntity, ...relatedEntities]) {
|
|
919
|
+
const entity = entityMap.get(path);
|
|
920
|
+
if (!entity) {
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
for (const prop of entity.properties) {
|
|
924
|
+
const values = valuesByEnum.get(prop.type);
|
|
925
|
+
if (values && values.length > 0) {
|
|
926
|
+
const shown = values.slice(0, MAX_VALID_VALUES_PER_PROPERTY).join(', ');
|
|
927
|
+
entries.push({
|
|
928
|
+
id: `valid_values:${entity.name}.${prop.name}`,
|
|
929
|
+
description: `Valid values for '${prop.name}' on ${entity.name}: ${shown}. Filter using these exact values (match case-insensitively).`,
|
|
930
|
+
category: 'valid_values',
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
return entries;
|
|
936
|
+
}
|
|
937
|
+
|
|
942
938
|
function buildNlContextEntries(
|
|
943
939
|
question: string,
|
|
944
940
|
rootEntity: string,
|
|
@@ -950,6 +946,14 @@ function buildNlContextEntries(
|
|
|
950
946
|
const entries: LegendAIAdditionalNlModelContext[] = [
|
|
951
947
|
...buildRootEntityNlHints(rootEntity, rootEntityObj, modelContext),
|
|
952
948
|
];
|
|
949
|
+
if (rootEntityObj) {
|
|
950
|
+
entries.unshift({
|
|
951
|
+
id: 'filter_guidance',
|
|
952
|
+
description:
|
|
953
|
+
'Filter guidance: match text/descriptive concepts on descriptive columns (e.g. description) using case-insensitive contains, not exact equals — and do NOT invent identifier/mnemonic codes (e.g. name, haverId, ticker) unless you were given their actual values. When several descriptive terms apply combine them with OR (never AND two contains on the same column). For country/geography prefer a coded column such as isoCode when one exists.',
|
|
954
|
+
category: 'filter_guidance',
|
|
955
|
+
});
|
|
956
|
+
}
|
|
953
957
|
if (modelContext.dataspaceDescription) {
|
|
954
958
|
const shortDesc = modelContext.dataspaceDescription
|
|
955
959
|
.split('\n')
|
|
@@ -978,6 +982,12 @@ function buildNlContextEntries(
|
|
|
978
982
|
entries.push(
|
|
979
983
|
...buildAssociationNlHints(rootEntity, relatedEntities, modelContext),
|
|
980
984
|
...buildExecutableNlHints(rootEntity, rootEntityObj, modelContext),
|
|
985
|
+
...buildValidValueNlHints(
|
|
986
|
+
rootEntity,
|
|
987
|
+
relatedEntities,
|
|
988
|
+
modelContext,
|
|
989
|
+
entityMap,
|
|
990
|
+
),
|
|
981
991
|
);
|
|
982
992
|
if (rootEntityObj) {
|
|
983
993
|
entries.push(
|
|
@@ -1158,19 +1168,13 @@ export function buildSemanticPropertyIndex(
|
|
|
1158
1168
|
const index = new Map<string, Set<string>>();
|
|
1159
1169
|
for (const entity of modelContext.entities) {
|
|
1160
1170
|
for (const prop of entity.properties) {
|
|
1161
|
-
const nameTokens = prop.name
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
.split(/[\s_]+/)
|
|
1165
|
-
.filter((t) => t.length > 2);
|
|
1171
|
+
const nameTokens = splitIdentifierTokens(prop.name).filter(
|
|
1172
|
+
(token) => token.length >= DEFAULT_MIN_TOKEN_LENGTH,
|
|
1173
|
+
);
|
|
1166
1174
|
addTokensToIndex(index, nameTokens, entity.path);
|
|
1167
1175
|
}
|
|
1168
1176
|
if (entity.description) {
|
|
1169
|
-
|
|
1170
|
-
.toLowerCase()
|
|
1171
|
-
.split(/[\s,.;:!?'"()\-/]+/)
|
|
1172
|
-
.filter((t) => t.length > 2);
|
|
1173
|
-
addTokensToIndex(index, descTokens, entity.path);
|
|
1177
|
+
addTokensToIndex(index, tokenizeText(entity.description), entity.path);
|
|
1174
1178
|
}
|
|
1175
1179
|
}
|
|
1176
1180
|
return index;
|
|
@@ -1180,6 +1184,8 @@ export function buildSemanticPropertyIndex(
|
|
|
1180
1184
|
// Deterministic entity resolution (no LLM required)
|
|
1181
1185
|
// ────────────────────────────────────────────────────────────────────────────
|
|
1182
1186
|
|
|
1187
|
+
const MAX_RESOLVED_RELATED_ENTITIES = 5;
|
|
1188
|
+
|
|
1183
1189
|
/**
|
|
1184
1190
|
* Resolves root and related entities using keyword matching against the model
|
|
1185
1191
|
* context. This works without any LLM call and is used as the final fallback
|
|
@@ -1199,7 +1205,7 @@ export function buildSemanticPropertyIndex(
|
|
|
1199
1205
|
function pickFallbackEntity(
|
|
1200
1206
|
queryableEntities: LegendAIModelEntity[],
|
|
1201
1207
|
entities: LegendAIModelEntity[],
|
|
1202
|
-
):
|
|
1208
|
+
): LegendAIResolvedEntities | undefined {
|
|
1203
1209
|
const preferred =
|
|
1204
1210
|
queryableEntities[0] ?? entities.find((e) => e.isRootMapped) ?? entities[0];
|
|
1205
1211
|
return preferred
|
|
@@ -1217,12 +1223,36 @@ function buildExecutableByRootIndex(
|
|
|
1217
1223
|
words = [];
|
|
1218
1224
|
index.set(exec.rootEntityPath, words);
|
|
1219
1225
|
}
|
|
1220
|
-
words.push(
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1226
|
+
words.push(...tokenizeText(`${exec.title} ${exec.description ?? ''}`));
|
|
1227
|
+
for (const param of exec.requiredParameters ?? []) {
|
|
1228
|
+
words.push(...tokenizeText(param.name));
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
return index;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function buildEnumValueIndex(
|
|
1235
|
+
modelContext: LegendAIModelContext,
|
|
1236
|
+
): Map<string, Set<string>> {
|
|
1237
|
+
const valueTokensByEnum = new Map<string, string[]>();
|
|
1238
|
+
for (const enumeration of modelContext.enumerations ?? []) {
|
|
1239
|
+
const valueTokens = enumeration.values.flatMap((value) =>
|
|
1240
|
+
tokenizeText(value),
|
|
1225
1241
|
);
|
|
1242
|
+
valueTokensByEnum.set(enumeration.path, valueTokens);
|
|
1243
|
+
valueTokensByEnum.set(enumeration.name, valueTokens);
|
|
1244
|
+
}
|
|
1245
|
+
const index = new Map<string, Set<string>>();
|
|
1246
|
+
if (valueTokensByEnum.size === 0) {
|
|
1247
|
+
return index;
|
|
1248
|
+
}
|
|
1249
|
+
for (const entity of modelContext.entities) {
|
|
1250
|
+
for (const prop of entity.properties) {
|
|
1251
|
+
const valueTokens = valueTokensByEnum.get(prop.type);
|
|
1252
|
+
if (valueTokens) {
|
|
1253
|
+
addTokensToIndex(index, valueTokens, entity.path);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1226
1256
|
}
|
|
1227
1257
|
return index;
|
|
1228
1258
|
}
|
|
@@ -1249,6 +1279,7 @@ function scoreEntity(
|
|
|
1249
1279
|
tokens: string[],
|
|
1250
1280
|
semanticIndex: Map<string, Set<string>>,
|
|
1251
1281
|
executableByRoot: Map<string, string[]>,
|
|
1282
|
+
enumValueIndex: Map<string, Set<string>>,
|
|
1252
1283
|
): number {
|
|
1253
1284
|
const nameLower = entity.name.toLowerCase();
|
|
1254
1285
|
let score =
|
|
@@ -1271,13 +1302,37 @@ function scoreEntity(
|
|
|
1271
1302
|
.length * 4;
|
|
1272
1303
|
}
|
|
1273
1304
|
score += tokens.filter((t) => semanticIndex.get(t)?.has(entity.path)).length;
|
|
1305
|
+
score +=
|
|
1306
|
+
tokens.filter((t) => enumValueIndex.get(t)?.has(entity.path)).length * 2;
|
|
1274
1307
|
return score;
|
|
1275
1308
|
}
|
|
1276
1309
|
|
|
1310
|
+
export function rankEntities(
|
|
1311
|
+
question: string,
|
|
1312
|
+
modelContext: LegendAIModelContext,
|
|
1313
|
+
): { entity: LegendAIModelEntity; score: number }[] {
|
|
1314
|
+
const executableByRoot = buildExecutableByRootIndex(modelContext);
|
|
1315
|
+
const semanticIndex = buildSemanticPropertyIndex(modelContext);
|
|
1316
|
+
const enumValueIndex = buildEnumValueIndex(modelContext);
|
|
1317
|
+
const tokens = tokenizeText(question);
|
|
1318
|
+
return modelContext.entities
|
|
1319
|
+
.map((entity) => ({
|
|
1320
|
+
entity,
|
|
1321
|
+
score: scoreEntity(
|
|
1322
|
+
entity,
|
|
1323
|
+
tokens,
|
|
1324
|
+
semanticIndex,
|
|
1325
|
+
executableByRoot,
|
|
1326
|
+
enumValueIndex,
|
|
1327
|
+
),
|
|
1328
|
+
}))
|
|
1329
|
+
.sort((a, b) => b.score - a.score);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1277
1332
|
export function resolveEntitiesDeterministic(
|
|
1278
1333
|
question: string,
|
|
1279
1334
|
modelContext: LegendAIModelContext,
|
|
1280
|
-
):
|
|
1335
|
+
): LegendAIResolvedEntities | undefined {
|
|
1281
1336
|
if (modelContext.entities.length === 0) {
|
|
1282
1337
|
return undefined;
|
|
1283
1338
|
}
|
|
@@ -1289,24 +1344,12 @@ export function resolveEntitiesDeterministic(
|
|
|
1289
1344
|
}
|
|
1290
1345
|
|
|
1291
1346
|
const queryableEntities = modelContext.entities.filter((e) => e.isQueryable);
|
|
1292
|
-
const executableByRoot = buildExecutableByRootIndex(modelContext);
|
|
1293
|
-
const semanticIndex = buildSemanticPropertyIndex(modelContext);
|
|
1294
|
-
|
|
1295
|
-
const tokens = question
|
|
1296
|
-
.toLowerCase()
|
|
1297
|
-
.split(/[\s,.;:!?'"()\-/]+/)
|
|
1298
|
-
.filter((t) => t.length > 2);
|
|
1299
1347
|
|
|
1300
|
-
if (
|
|
1348
|
+
if (tokenizeText(question).length === 0) {
|
|
1301
1349
|
return pickFallbackEntity(queryableEntities, modelContext.entities);
|
|
1302
1350
|
}
|
|
1303
1351
|
|
|
1304
|
-
const scored = modelContext
|
|
1305
|
-
.map((entity) => ({
|
|
1306
|
-
entity,
|
|
1307
|
-
score: scoreEntity(entity, tokens, semanticIndex, executableByRoot),
|
|
1308
|
-
}))
|
|
1309
|
-
.sort((a, b) => b.score - a.score);
|
|
1352
|
+
const scored = rankEntities(question, modelContext);
|
|
1310
1353
|
|
|
1311
1354
|
const rootCandidates =
|
|
1312
1355
|
queryableEntities.length > 0
|
|
@@ -1328,17 +1371,143 @@ export function resolveEntitiesDeterministic(
|
|
|
1328
1371
|
}
|
|
1329
1372
|
}
|
|
1330
1373
|
for (const s of scored.slice(1)) {
|
|
1331
|
-
if (s.score > 0 && relatedSet.size <
|
|
1374
|
+
if (s.score > 0 && relatedSet.size < MAX_RESOLVED_RELATED_ENTITIES) {
|
|
1332
1375
|
relatedSet.add(s.entity.path);
|
|
1333
1376
|
}
|
|
1334
1377
|
}
|
|
1335
1378
|
|
|
1336
1379
|
return {
|
|
1337
1380
|
rootEntity: rootPath,
|
|
1338
|
-
relatedEntities: Array.from(relatedSet).slice(
|
|
1381
|
+
relatedEntities: Array.from(relatedSet).slice(
|
|
1382
|
+
0,
|
|
1383
|
+
MAX_RESOLVED_RELATED_ENTITIES,
|
|
1384
|
+
),
|
|
1339
1385
|
};
|
|
1340
1386
|
}
|
|
1341
1387
|
|
|
1388
|
+
const MAX_APPROACH_COLUMNS = 6;
|
|
1389
|
+
const MAX_APPROACH_DESCRIPTION_LENGTH = 200;
|
|
1390
|
+
const MAX_DESCRIPTION_PREVIEW_LENGTH = 300;
|
|
1391
|
+
|
|
1392
|
+
export function buildDataQueryApproachText(
|
|
1393
|
+
question: string,
|
|
1394
|
+
modelContext: LegendAIModelContext,
|
|
1395
|
+
rootEntityPath?: string,
|
|
1396
|
+
): string | undefined {
|
|
1397
|
+
const rootPath =
|
|
1398
|
+
rootEntityPath ??
|
|
1399
|
+
resolveEntitiesDeterministic(question, modelContext)?.rootEntity;
|
|
1400
|
+
if (!rootPath) {
|
|
1401
|
+
return undefined;
|
|
1402
|
+
}
|
|
1403
|
+
const root = modelContext.entities.find((entity) => entity.path === rootPath);
|
|
1404
|
+
if (!root) {
|
|
1405
|
+
return undefined;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
const service = modelContext.executables?.find(
|
|
1409
|
+
(exec) => exec.rootEntityPath === root.path,
|
|
1410
|
+
)?.title;
|
|
1411
|
+
const columns = root.properties
|
|
1412
|
+
.filter((prop) => !prop.type.includes('::'))
|
|
1413
|
+
.map((prop) => prop.name);
|
|
1414
|
+
const relatedPaths = new Set<string>();
|
|
1415
|
+
for (const assoc of modelContext.associations) {
|
|
1416
|
+
if (assoc.leftEntity === root.path) {
|
|
1417
|
+
relatedPaths.add(assoc.rightEntity);
|
|
1418
|
+
} else if (assoc.rightEntity === root.path) {
|
|
1419
|
+
relatedPaths.add(assoc.leftEntity);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
const related = Array.from(relatedPaths).map((path) =>
|
|
1423
|
+
extractElementNameFromPath(path),
|
|
1424
|
+
);
|
|
1425
|
+
|
|
1426
|
+
const sentences: string[] = [];
|
|
1427
|
+
const via = service ? ` via the **${service}** service` : '';
|
|
1428
|
+
const description = root.description
|
|
1429
|
+
? ` — ${root.description.slice(0, MAX_APPROACH_DESCRIPTION_LENGTH).trim()}`
|
|
1430
|
+
: '';
|
|
1431
|
+
sentences.push(
|
|
1432
|
+
`To answer this, I'll query **${root.name}**${via}${description}.`,
|
|
1433
|
+
);
|
|
1434
|
+
|
|
1435
|
+
if (columns.length > 0) {
|
|
1436
|
+
const shown = columns
|
|
1437
|
+
.slice(0, MAX_APPROACH_COLUMNS)
|
|
1438
|
+
.map((name) => `\`${name}\``)
|
|
1439
|
+
.join(', ');
|
|
1440
|
+
const more =
|
|
1441
|
+
columns.length > MAX_APPROACH_COLUMNS
|
|
1442
|
+
? ` (and ${columns.length - MAX_APPROACH_COLUMNS} more)`
|
|
1443
|
+
: '';
|
|
1444
|
+
sentences.push(`It exposes fields such as ${shown}${more}.`);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
if (related.length > 0) {
|
|
1448
|
+
const list = related.map((name) => `**${name}**`).join(', ');
|
|
1449
|
+
sentences.push(
|
|
1450
|
+
`Where the question needs more, results can be enriched by joining related ${
|
|
1451
|
+
related.length > 1 ? 'entities' : 'entity'
|
|
1452
|
+
} ${list} through the model's associations.`,
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
return sentences.join(' ');
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
const EXACT_STRING_FILTER_PATTERN =
|
|
1460
|
+
/(?<lhs>\$\w+(?:\.\w+)+(?:->toOne\(\))?)\s*==\s*'(?<literal>[^']*)'/g;
|
|
1461
|
+
|
|
1462
|
+
export function relaxExactStringFilters(pureQuery: string): string {
|
|
1463
|
+
return pureQuery.replace(
|
|
1464
|
+
EXACT_STRING_FILTER_PATTERN,
|
|
1465
|
+
(_match, lhs: string, literal: string) =>
|
|
1466
|
+
`${lhs}->toLower()->contains('${literal.toLowerCase()}')`,
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
const STRING_COMPARISON_COLUMN_PATTERN =
|
|
1471
|
+
/\$\w+(?:\.\w+)*\.(?<col>\w+)(?:->toOne\(\))?\s*(?:==|!=|<=|>=|<|>)\s*'/g;
|
|
1472
|
+
const CONTAINS_COLUMN_PATTERN =
|
|
1473
|
+
/\$\w+(?:\.\w+)*\.(?<col>\w+)(?:->to\w+\(\))*->(?:contains|startsWith|endsWith)\(/g;
|
|
1474
|
+
|
|
1475
|
+
export function extractFilteredColumns(pureQuery: string): string[] {
|
|
1476
|
+
const columns = new Set<string>();
|
|
1477
|
+
for (const pattern of [
|
|
1478
|
+
STRING_COMPARISON_COLUMN_PATTERN,
|
|
1479
|
+
CONTAINS_COLUMN_PATTERN,
|
|
1480
|
+
]) {
|
|
1481
|
+
for (const match of pureQuery.matchAll(pattern)) {
|
|
1482
|
+
const col = match.groups?.col;
|
|
1483
|
+
if (col) {
|
|
1484
|
+
columns.add(col);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
return Array.from(columns);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
const MAX_PROBED_VALUES_PER_COLUMN = 30;
|
|
1492
|
+
|
|
1493
|
+
export function buildProbedValueHints(
|
|
1494
|
+
probed: Map<string, string[]>,
|
|
1495
|
+
): LegendAIAdditionalNlModelContext[] {
|
|
1496
|
+
const entries: LegendAIAdditionalNlModelContext[] = [];
|
|
1497
|
+
for (const [column, values] of probed) {
|
|
1498
|
+
if (values.length === 0) {
|
|
1499
|
+
continue;
|
|
1500
|
+
}
|
|
1501
|
+
const shown = values.slice(0, MAX_PROBED_VALUES_PER_COLUMN).join(', ');
|
|
1502
|
+
entries.push({
|
|
1503
|
+
id: `probed_values:${column}`,
|
|
1504
|
+
description: `Actual values present in column '${column}': ${shown}. Filter using one of these exact values (case-insensitively).`,
|
|
1505
|
+
category: 'probed_values',
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
return entries;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1342
1511
|
// ────────────────────────────────────────────────────────────────────────────
|
|
1343
1512
|
// Model context enrichment for SQL / metadata prompts
|
|
1344
1513
|
// ────────────────────────────────────────────────────────────────────────────
|
|
@@ -1348,6 +1517,9 @@ const MAX_ENRICHMENT_PROPS_PER_ENTITY = 15;
|
|
|
1348
1517
|
const MAX_ENRICHMENT_ENUMS = 10;
|
|
1349
1518
|
const MAX_ENRICHMENT_ENUM_VALUES = 20;
|
|
1350
1519
|
const MAX_ENRICHMENT_ASSOCIATIONS = 10;
|
|
1520
|
+
const MAX_ENRICHMENT_EXECUTABLES = 25;
|
|
1521
|
+
const MAX_ENRICHMENT_JOIN_SERVICES = 25;
|
|
1522
|
+
const MIN_SERVICE_ENTITY_COLUMN_OVERLAP = 2;
|
|
1351
1523
|
|
|
1352
1524
|
/**
|
|
1353
1525
|
* Maps each TDS service to the model entity whose properties best match
|
|
@@ -1363,7 +1535,6 @@ function inferServiceEntityMapping(
|
|
|
1363
1535
|
modelContext: LegendAIModelContext,
|
|
1364
1536
|
): Map<string, string> {
|
|
1365
1537
|
const result = new Map<string, string>();
|
|
1366
|
-
const MIN_OVERLAP = 2; // require at least 2 matching columns
|
|
1367
1538
|
|
|
1368
1539
|
const entityPropSets = modelContext.entities.map((entity) => ({
|
|
1369
1540
|
path: entity.path,
|
|
@@ -1383,7 +1554,7 @@ function inferServiceEntityMapping(
|
|
|
1383
1554
|
}
|
|
1384
1555
|
}
|
|
1385
1556
|
|
|
1386
|
-
if (bestScore >=
|
|
1557
|
+
if (bestScore >= MIN_SERVICE_ENTITY_COLUMN_OVERLAP && bestEntity) {
|
|
1387
1558
|
result.set(svc.title, bestEntity);
|
|
1388
1559
|
}
|
|
1389
1560
|
}
|
|
@@ -1431,7 +1602,7 @@ function buildEntitySection(
|
|
|
1431
1602
|
const tagStr = tags.length > 0 ? ` [${tags.join(', ')}]` : '';
|
|
1432
1603
|
lines.push(`\n### ${entity.name}${tagStr}`);
|
|
1433
1604
|
if (entity.description) {
|
|
1434
|
-
lines.push(entity.description.slice(0,
|
|
1605
|
+
lines.push(entity.description.slice(0, MAX_DESCRIPTION_PREVIEW_LENGTH));
|
|
1435
1606
|
}
|
|
1436
1607
|
const propLines = entity.properties
|
|
1437
1608
|
.slice(0, MAX_ENRICHMENT_PROPS_PER_ENTITY)
|
|
@@ -1493,7 +1664,10 @@ function buildExecutableSection(
|
|
|
1493
1664
|
return undefined;
|
|
1494
1665
|
}
|
|
1495
1666
|
const lines = ['## Available Executables'];
|
|
1496
|
-
for (const exec of modelContext.executables
|
|
1667
|
+
for (const exec of modelContext.executables.slice(
|
|
1668
|
+
0,
|
|
1669
|
+
MAX_ENRICHMENT_EXECUTABLES,
|
|
1670
|
+
)) {
|
|
1497
1671
|
lines.push(
|
|
1498
1672
|
`\n- "${exec.title}" → ${extractElementNameFromPath(exec.rootEntityPath)}`,
|
|
1499
1673
|
);
|
|
@@ -1507,6 +1681,11 @@ function buildExecutableSection(
|
|
|
1507
1681
|
lines.push(` Required parameters: ${paramList}`);
|
|
1508
1682
|
}
|
|
1509
1683
|
}
|
|
1684
|
+
if (modelContext.executables.length > MAX_ENRICHMENT_EXECUTABLES) {
|
|
1685
|
+
lines.push(
|
|
1686
|
+
`\n(${modelContext.executables.length - MAX_ENRICHMENT_EXECUTABLES} additional executables omitted)`,
|
|
1687
|
+
);
|
|
1688
|
+
}
|
|
1510
1689
|
return lines.join('\n');
|
|
1511
1690
|
}
|
|
1512
1691
|
|
|
@@ -1659,15 +1838,10 @@ function buildServicePairHint(
|
|
|
1659
1838
|
}
|
|
1660
1839
|
const leftName = extractElementNameFromPath(assoc.leftEntity);
|
|
1661
1840
|
const rightName = extractElementNameFromPath(assoc.rightEntity);
|
|
1662
|
-
const
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
);
|
|
1667
|
-
const svcBCols = services.find((s) => s.title === svcB)?.columns ?? [];
|
|
1668
|
-
const shared = svcBCols
|
|
1669
|
-
.filter((c) => colsA.has(c.name.toLowerCase()))
|
|
1670
|
-
.map((c) => c.name);
|
|
1841
|
+
const serviceA = services.find((s) => s.title === svcA);
|
|
1842
|
+
const serviceB = services.find((s) => s.title === svcB);
|
|
1843
|
+
const shared =
|
|
1844
|
+
serviceA && serviceB ? sharedColumnNames(serviceB, serviceA) : [];
|
|
1671
1845
|
const joinKeyHint =
|
|
1672
1846
|
shared.length > 0
|
|
1673
1847
|
? `JOIN ON shared columns: ${shared.join(', ')}`
|
|
@@ -1721,19 +1895,26 @@ function buildServiceJoinSection(
|
|
|
1721
1895
|
if (serviceEntityMap.size === 0) {
|
|
1722
1896
|
return undefined;
|
|
1723
1897
|
}
|
|
1898
|
+
const allEntries = Array.from(serviceEntityMap);
|
|
1899
|
+
const cappedEntries = allEntries.slice(0, MAX_ENRICHMENT_JOIN_SERVICES);
|
|
1724
1900
|
const joinLines: string[] = [
|
|
1725
1901
|
'## Model-Aware Service JOIN Guide',
|
|
1726
1902
|
'Each service below is mapped to the data model entity whose properties best match its columns.',
|
|
1727
1903
|
'Use the entity relationships to determine correct JOIN keys and semantics.',
|
|
1728
1904
|
'',
|
|
1729
1905
|
];
|
|
1730
|
-
for (const [svcTitle, entityPath] of
|
|
1906
|
+
for (const [svcTitle, entityPath] of cappedEntries) {
|
|
1731
1907
|
joinLines.push(
|
|
1732
1908
|
`- **${svcTitle}** → entity ${extractElementNameFromPath(entityPath)}`,
|
|
1733
1909
|
);
|
|
1734
1910
|
}
|
|
1911
|
+
if (allEntries.length > MAX_ENRICHMENT_JOIN_SERVICES) {
|
|
1912
|
+
joinLines.push(
|
|
1913
|
+
`\n(${allEntries.length - MAX_ENRICHMENT_JOIN_SERVICES} additional services omitted)`,
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1735
1916
|
const pairHints = buildServicePairHints(
|
|
1736
|
-
|
|
1917
|
+
new Map(cappedEntries),
|
|
1737
1918
|
modelContext,
|
|
1738
1919
|
services,
|
|
1739
1920
|
);
|
|
@@ -1767,3 +1948,160 @@ The following describes the underlying data model — entities, properties, enum
|
|
|
1767
1948
|
|
|
1768
1949
|
${sections.join('\n\n')}`;
|
|
1769
1950
|
}
|
|
1951
|
+
|
|
1952
|
+
const MAX_CATALOG_ENTITIES = 40;
|
|
1953
|
+
const MAX_CATALOG_PROPS_PER_ENTITY = 50;
|
|
1954
|
+
const MAX_CATALOG_ENUMS = 30;
|
|
1955
|
+
const MAX_CATALOG_ENUM_VALUES = 50;
|
|
1956
|
+
const MAX_CATALOG_ASSOCIATIONS = 40;
|
|
1957
|
+
const MAX_CATALOG_EXECUTABLES = 30;
|
|
1958
|
+
|
|
1959
|
+
/** Formats a property's multiplicity marker from its cardinality flags. */
|
|
1960
|
+
function formatCatalogMultiplicity(property: LegendAIModelProperty): string {
|
|
1961
|
+
if (property.isCollection) {
|
|
1962
|
+
return '[*]';
|
|
1963
|
+
}
|
|
1964
|
+
return property.isOptional ? '[0..1]' : '[1]';
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
/** Builds the entities-and-columns section of the model catalog. */
|
|
1968
|
+
function buildCatalogEntitiesSection(
|
|
1969
|
+
modelContext: LegendAIModelContext,
|
|
1970
|
+
): string | undefined {
|
|
1971
|
+
if (modelContext.entities.length === 0) {
|
|
1972
|
+
return undefined;
|
|
1973
|
+
}
|
|
1974
|
+
const scoreOf = (e: LegendAIModelEntity): number =>
|
|
1975
|
+
(e.isQueryable ? 10 : 0) + (e.isRootMapped ? 3 : 0);
|
|
1976
|
+
const sorted = [...modelContext.entities].sort(
|
|
1977
|
+
(a, b) => scoreOf(b) - scoreOf(a),
|
|
1978
|
+
);
|
|
1979
|
+
const lines: string[] = ['## Entities and Columns'];
|
|
1980
|
+
for (const entity of sorted.slice(0, MAX_CATALOG_ENTITIES)) {
|
|
1981
|
+
const tags: string[] = [];
|
|
1982
|
+
if (entity.isQueryable) {
|
|
1983
|
+
tags.push('queryable');
|
|
1984
|
+
}
|
|
1985
|
+
if (entity.isRootMapped) {
|
|
1986
|
+
tags.push('root-mapped');
|
|
1987
|
+
}
|
|
1988
|
+
const tagStr = tags.length > 0 ? ` [${tags.join(', ')}]` : '';
|
|
1989
|
+
lines.push(`\n### ${entity.name}${tagStr}`);
|
|
1990
|
+
if (entity.description) {
|
|
1991
|
+
lines.push(entity.description);
|
|
1992
|
+
}
|
|
1993
|
+
const propLines = entity.properties
|
|
1994
|
+
.slice(0, MAX_CATALOG_PROPS_PER_ENTITY)
|
|
1995
|
+
.map((p) => `- ${p.name}: ${p.type} ${formatCatalogMultiplicity(p)}`);
|
|
1996
|
+
if (propLines.length > 0) {
|
|
1997
|
+
lines.push('Columns:', ...propLines);
|
|
1998
|
+
}
|
|
1999
|
+
if (entity.properties.length > MAX_CATALOG_PROPS_PER_ENTITY) {
|
|
2000
|
+
lines.push(
|
|
2001
|
+
`- (${entity.properties.length - MAX_CATALOG_PROPS_PER_ENTITY} more columns)`,
|
|
2002
|
+
);
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
if (modelContext.entities.length > MAX_CATALOG_ENTITIES) {
|
|
2006
|
+
lines.push(
|
|
2007
|
+
`\n(${modelContext.entities.length - MAX_CATALOG_ENTITIES} additional entities omitted — ask about a specific one for its columns)`,
|
|
2008
|
+
);
|
|
2009
|
+
}
|
|
2010
|
+
return lines.join('\n');
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
/** Builds the relationships section of the model catalog. */
|
|
2014
|
+
function buildCatalogRelationshipsSection(
|
|
2015
|
+
modelContext: LegendAIModelContext,
|
|
2016
|
+
): string | undefined {
|
|
2017
|
+
if (modelContext.associations.length === 0) {
|
|
2018
|
+
return undefined;
|
|
2019
|
+
}
|
|
2020
|
+
const lines: string[] = ['## Relationships'];
|
|
2021
|
+
for (const assoc of modelContext.associations.slice(
|
|
2022
|
+
0,
|
|
2023
|
+
MAX_CATALOG_ASSOCIATIONS,
|
|
2024
|
+
)) {
|
|
2025
|
+
lines.push(
|
|
2026
|
+
`- ${assoc.leftEntity}.${assoc.leftProperty} <-> ${assoc.rightEntity}.${assoc.rightProperty}`,
|
|
2027
|
+
);
|
|
2028
|
+
}
|
|
2029
|
+
if (modelContext.associations.length > MAX_CATALOG_ASSOCIATIONS) {
|
|
2030
|
+
lines.push(
|
|
2031
|
+
`- (${modelContext.associations.length - MAX_CATALOG_ASSOCIATIONS} more relationships)`,
|
|
2032
|
+
);
|
|
2033
|
+
}
|
|
2034
|
+
return lines.join('\n');
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
/** Builds the enumerations (valid values) section of the model catalog. */
|
|
2038
|
+
function buildCatalogEnumerationsSection(
|
|
2039
|
+
modelContext: LegendAIModelContext,
|
|
2040
|
+
): string | undefined {
|
|
2041
|
+
const enumerations = modelContext.enumerations ?? [];
|
|
2042
|
+
if (enumerations.length === 0) {
|
|
2043
|
+
return undefined;
|
|
2044
|
+
}
|
|
2045
|
+
const lines: string[] = ['## Enumerations (valid values)'];
|
|
2046
|
+
for (const en of enumerations.slice(0, MAX_CATALOG_ENUMS)) {
|
|
2047
|
+
const shown = en.values.slice(0, MAX_CATALOG_ENUM_VALUES).join(', ');
|
|
2048
|
+
const more =
|
|
2049
|
+
en.values.length > MAX_CATALOG_ENUM_VALUES
|
|
2050
|
+
? `, ... (${en.values.length} total)`
|
|
2051
|
+
: '';
|
|
2052
|
+
lines.push(`- ${en.name}: ${shown}${more}`);
|
|
2053
|
+
}
|
|
2054
|
+
return lines.join('\n');
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
/** Builds the available-executables section of the model catalog. */
|
|
2058
|
+
function buildCatalogExecutablesSection(
|
|
2059
|
+
modelContext: LegendAIModelContext,
|
|
2060
|
+
): string | undefined {
|
|
2061
|
+
const executables = modelContext.executables ?? [];
|
|
2062
|
+
if (executables.length === 0) {
|
|
2063
|
+
return undefined;
|
|
2064
|
+
}
|
|
2065
|
+
const lines: string[] = ['## How to query (available executables)'];
|
|
2066
|
+
for (const exec of executables.slice(0, MAX_CATALOG_EXECUTABLES)) {
|
|
2067
|
+
const root = exec.rootEntityPath
|
|
2068
|
+
? ` → ${extractElementNameFromPath(exec.rootEntityPath)}`
|
|
2069
|
+
: '';
|
|
2070
|
+
lines.push(`\n- **${exec.title}**${root}`);
|
|
2071
|
+
if (exec.description) {
|
|
2072
|
+
lines.push(
|
|
2073
|
+
` ${exec.description.slice(0, MAX_DESCRIPTION_PREVIEW_LENGTH)}`,
|
|
2074
|
+
);
|
|
2075
|
+
}
|
|
2076
|
+
if (exec.requiredParameters && exec.requiredParameters.length > 0) {
|
|
2077
|
+
const paramList = exec.requiredParameters
|
|
2078
|
+
.map((p) => `${p.name} (${p.type})`)
|
|
2079
|
+
.join(', ');
|
|
2080
|
+
lines.push(` Required parameters: ${paramList}`);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
if (executables.length > MAX_CATALOG_EXECUTABLES) {
|
|
2084
|
+
lines.push(`\n(${executables.length - MAX_CATALOG_EXECUTABLES} more)`);
|
|
2085
|
+
}
|
|
2086
|
+
return lines.join('\n');
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
export function buildModelCatalogText(
|
|
2090
|
+
modelContext: LegendAIModelContext,
|
|
2091
|
+
): string | undefined {
|
|
2092
|
+
const sections = [
|
|
2093
|
+
buildCatalogEntitiesSection(modelContext),
|
|
2094
|
+
buildCatalogRelationshipsSection(modelContext),
|
|
2095
|
+
buildCatalogEnumerationsSection(modelContext),
|
|
2096
|
+
buildCatalogExecutablesSection(modelContext),
|
|
2097
|
+
].filter(isNonNullable);
|
|
2098
|
+
|
|
2099
|
+
if (sections.length === 0) {
|
|
2100
|
+
return undefined;
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
return `# MODEL REFERENCE
|
|
2104
|
+
A reference for this data product's model. Use it to explain the entities and their columns, how entities relate, valid enumeration values, and how to query the data. Give a thorough, well-structured answer.
|
|
2105
|
+
|
|
2106
|
+
${sections.join('\n\n')}`;
|
|
2107
|
+
}
|