Skip to content

Commit 5319627

Browse files
authored
fix: support GenAiPlanner and GenAiPlannerBundle for agent pseudotype (#1542)
1 parent 178d2e2 commit 5319627

16 files changed

+1019
-55
lines changed

src/resolve/pseudoTypes/agentResolver.ts

Lines changed: 58 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
*/
77

88
import { readFileSync } from 'node:fs';
9+
import { join } from 'node:path';
910
import { XMLParser } from 'fast-xml-parser';
1011
import { Connection, Logger, SfError, trimTo15 } from '@salesforce/core';
11-
import type { BotVersion, GenAiPlanner, GenAiPlugin } from '@salesforce/types/metadata';
12+
import type { BotVersion, GenAiPlanner, GenAiPlannerFunctionDef, GenAiPlugin } from '@salesforce/types/metadata';
1213
import { ensureArray } from '@salesforce/kit';
1314
import { RegistryAccess } from '../../registry';
1415
import { ComponentSet } from '../../collections/componentSet';
15-
import { SourceComponent } from '../sourceComponent';
1616
import { MetadataComponent } from '../types';
1717

1818
type BotVersionExt = {
@@ -61,6 +61,9 @@ export async function resolveAgentMdEntries(agentMdInfo: {
6161
registry?: RegistryAccess;
6262
}): Promise<string[]> {
6363
const { botName, connection, directoryPaths } = agentMdInfo;
64+
const v63topLevelMd = ['Bot', 'GenAiPlanner', 'GenAiPlugin', 'GenAiFunction'];
65+
const v64topLevelMd = ['Bot', 'GenAiPlannerBundle', 'GenAiPlugin', 'GenAiFunction'];
66+
6467
let debugMsg = `Resolving agent metadata with botName: ${botName}`;
6568
if (connection) {
6669
debugMsg += ` and org connection ${connection.getUsername() as string}`;
@@ -72,7 +75,10 @@ export async function resolveAgentMdEntries(agentMdInfo: {
7275

7376
if (botName === '*') {
7477
// Get all Agent top level metadata
75-
return Promise.resolve(['Bot', 'GenAiPlanner', 'GenAiPlugin', 'GenAiFunction']);
78+
if (connection && Number(connection.getApiVersion()) > 63.0) {
79+
return Promise.resolve(v64topLevelMd);
80+
}
81+
return Promise.resolve(v63topLevelMd);
7682
}
7783

7884
if (connection) {
@@ -99,7 +105,8 @@ const resolveAgentFromConnection = async (connection: Connection, botName: strin
99105
const plannerId = (await connection.singleRecordQuery<{ Id: string }>(genAiPlannerIdQuery, { tooling: true })).Id;
100106

101107
if (plannerId) {
102-
mdEntries.push(`GenAiPlanner:${botName}`);
108+
const plannerType = Number(connection.getApiVersion()) > 63.0 ? 'GenAiPlannerBundle' : 'GenAiPlanner';
109+
mdEntries.push(`${plannerType}:${botName}`);
103110
const plannerId15 = trimTo15(plannerId);
104111
// Query for the GenAiPlugins associated with the 15 char GenAiPlannerId
105112
const genAiPluginNames = (
@@ -111,11 +118,11 @@ const resolveAgentFromConnection = async (connection: Connection, botName: strin
111118
genAiPluginNames.map((r) => mdEntries.push(`GenAiPlugin:${r.DeveloperName}`));
112119
} else {
113120
getLogger().debug(
114-
`No GenAiPlugin metadata matches for plannerId: ${plannerId15}. Reading the planner metadata for plugins...`
121+
`No GenAiPlugin metadata matches for plannerId: ${plannerId15}. Reading the ${plannerType} metadata for plugins...`
115122
);
116123
// read the planner metadata from the org
117124
// @ts-expect-error jsForce types don't know about GenAiPlanner yet
118-
const genAiPlannerMd = await connection.metadata.read<GenAiPlanner>('GenAiPlanner', botName);
125+
const genAiPlannerMd = await connection.metadata.read<GenAiPlanner>(plannerType, botName);
119126
const genAiPlannerMdArr = ensureArray(genAiPlannerMd) as unknown as GenAiPlanner[];
120127
if (genAiPlannerMdArr?.length && genAiPlannerMdArr[0]?.genAiPlugins.length) {
121128
genAiPlannerMdArr[0].genAiPlugins.map((plugin) => {
@@ -162,19 +169,20 @@ const resolveAgentFromLocalMetadata = (
162169
}
163170
const parser = new XMLParser({ ignoreAttributes: false });
164171
const botFiles = botCompSet.getComponentFilenamesByNameAndType({ type: 'Bot', fullName: botName });
165-
const plannerType = registry.getTypeByName('GenAiPlanner');
172+
let plannerType = registry.getTypeByName('GenAiPlannerBundle');
173+
let plannerTypeName = 'GenAiPlannerBundle';
166174
let plannerCompSet = ComponentSet.fromSource({
167175
fsPaths: directoryPaths,
168176
include: new ComponentSet([{ type: plannerType, fullName: botName }], registry),
169177
registry,
170178
});
171-
// If the plannerCompSet is empty it might be due to the GenAiPlanner having a
179+
// If the plannerCompSet is empty it might be due to the GenAiPlannerBundle having a
172180
// different name than the Bot. We need to search the BotVersion for the
173181
// planner API name.
174182
if (plannerCompSet.size < 1) {
175183
const botVersionFile = botFiles.find((botFile) => botFile.endsWith('.botVersion-meta.xml'));
176184
if (botVersionFile) {
177-
getLogger().debug(`Reading and parsing ${botVersionFile} to find all GenAiPlanner references`);
185+
getLogger().debug(`Reading and parsing ${botVersionFile} to find all GenAiPlanner/GenAiPlannerBundle references`);
178186
const botVersionJson = xmlToJson<BotVersionExt>(botVersionFile, parser);
179187
// Per the schema, there can be multiple GenAiPlanners linked to a BotVersion
180188
// but I'm not sure how that would work so for now just using the first one.
@@ -186,36 +194,61 @@ const resolveAgentFromLocalMetadata = (
186194
include: new ComponentSet([{ type: plannerType, fullName: genAiPlannerName }], registry),
187195
registry,
188196
});
197+
// If the plannerCompSet is empty look for a GenAiPlanner
189198
if (plannerCompSet.size < 1) {
190-
getLogger().debug(`Cannot find GenAiPlanner with name: ${genAiPlannerName}`);
199+
plannerTypeName = 'GenAiPlanner';
200+
plannerType = registry.getTypeByName('GenAiPlanner');
201+
plannerCompSet = ComponentSet.fromSource({
202+
fsPaths: directoryPaths,
203+
include: new ComponentSet([{ type: plannerType, fullName: genAiPlannerName }], registry),
204+
registry,
205+
});
206+
if (plannerCompSet.size < 1) {
207+
getLogger().debug(`Cannot find GenAiPlanner or GenAiPlannerBundle with name: ${genAiPlannerName}`);
208+
}
191209
}
192-
getLogger().debug(`Adding GenAiPlanner:${genAiPlannerName}`);
193-
mdEntries.add(`GenAiPlanner:${genAiPlannerName}`);
210+
getLogger().debug(`Adding ${plannerTypeName}:${genAiPlannerName}`);
211+
mdEntries.add(`${plannerTypeName}:${genAiPlannerName}`);
194212
} else {
195213
getLogger().debug(`Cannot find GenAiPlannerName in BotVersion file: ${botVersionFile}`);
196214
}
197215
}
198216
} else {
199-
getLogger().debug(`Adding GenAiPlanner:${botName}`);
200-
mdEntries.add(`GenAiPlanner:${botName}`);
217+
getLogger().debug(`Adding ${plannerTypeName}:${botName}`);
218+
mdEntries.add(`${plannerTypeName}:${botName}`);
201219
}
202220

203-
// Read the GenAiPlanner file for GenAiPlugins
204-
const plannerComp = plannerCompSet.find((mdComp) => mdComp.type.name === 'GenAiPlanner');
205-
if (plannerComp && 'xml' in plannerComp) {
206-
const plannerFile = (plannerComp as SourceComponent).xml;
221+
// Read the GenAiPlanner or GenAiPlannerBundle file for GenAiPlugins
222+
const plannerComp = plannerCompSet.getSourceComponents().first();
223+
if (plannerComp) {
224+
let plannerFilePath;
225+
if (plannerTypeName === 'GenAiPlannerBundle' && plannerComp.content) {
226+
const plannerFileName = plannerComp.tree
227+
.readDirectory(plannerComp.content)
228+
.find((p) => p.endsWith('.genAiPlannerBundle'));
229+
if (plannerFileName) {
230+
plannerFilePath = join(plannerComp.content, plannerFileName);
231+
} else {
232+
getLogger().debug(`Cannot find GenAiPlannerBundle file in ${plannerComp.content}`);
233+
}
234+
} else {
235+
plannerFilePath = plannerComp.xml;
236+
}
237+
207238
// Certain internal plugins and functions cannot be retrieved/deployed so don't include them.
208-
const internalPrefix = 'EmployeeCopilot__';
209-
if (plannerFile) {
210-
getLogger().debug(`Reading and parsing ${plannerFile} to find all GenAiPlugin references`);
211-
const plannerJson = xmlToJson<GenAiPlannerExt>(plannerFile, parser);
239+
const internalPrefixes = ['EmployeeCopilot__', 'SvcCopilotTmpl__'];
240+
if (plannerFilePath) {
241+
getLogger().debug(`Reading and parsing ${plannerFilePath} to find all GenAiPlugin references`);
242+
const plannerJson = xmlToJson<GenAiPlannerExt>(plannerFilePath, parser);
212243

213244
// Add plugins defined in the planner
214-
const genAiPlugins = ensureArray(plannerJson.GenAiPlanner.genAiPlugins);
245+
// @ts-expect-error temporary until GenAiPlannerBundle metadata type is defined
246+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
247+
const genAiPlugins = ensureArray(plannerJson[plannerTypeName].genAiPlugins) as GenAiPlannerFunctionDef[];
215248
const pluginType = registry.getTypeByName('GenAiPlugin');
216249
const genAiPluginComps: MetadataComponent[] = [];
217250
genAiPlugins?.map((plugin) => {
218-
if (plugin.genAiPluginName && !plugin.genAiPluginName.startsWith(internalPrefix)) {
251+
if (plugin.genAiPluginName && !internalPrefixes.some((prefix) => plugin.genAiPluginName?.startsWith(prefix))) {
219252
genAiPluginComps.push({ type: pluginType, fullName: plugin.genAiPluginName });
220253
getLogger().debug(`Adding GenAiPlugin:${plugin.genAiPluginName}`);
221254
mdEntries.add(`GenAiPlugin:${plugin.genAiPluginName}`);
@@ -237,7 +270,7 @@ const resolveAgentFromLocalMetadata = (
237270
const genAiPlugin = xmlToJson<GenAiPluginExt>(comp.xml, parser);
238271
const genAiFunctions = ensureArray(genAiPlugin.GenAiPlugin.genAiFunctions);
239272
genAiFunctions.map((func) => {
240-
if (func.functionName && !func.functionName.startsWith(internalPrefix)) {
273+
if (func.functionName && !internalPrefixes.some((prefix) => func.functionName.startsWith(prefix))) {
241274
getLogger().debug(`Adding GenAiFunction:${func.functionName}`);
242275
mdEntries.add(`GenAiFunction:${func.functionName}`);
243276
}

test/collections/componentSetBuilder.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,9 @@ describe('ComponentSetBuilder', () => {
481481
const fromSourceArg = fromSourceStub.thirdCall.firstArg;
482482
expect(fromSourceArg).to.have.deep.property('fsPaths', [packageDir1]);
483483
expect(fromSourceArg).to.have.property('include');
484-
const expectedComps = ['bot#MyBot', 'genaiplanner#MyBot'];
484+
// expect genaiplannerbundle because it's now the default and the stubbing
485+
// for this test is very naive.
486+
const expectedComps = ['bot#MyBot', 'genaiplannerbundle#MyBot'];
485487
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
486488
expect(Array.from(fromSourceArg.include.components.keys())).to.deep.equal(expectedComps);
487489
expect(compSet.getSourceComponents()).to.deep.equal(mdCompSet.getSourceComponents());

test/nuts/agents/agentResolver.test.ts

Lines changed: 68 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@ config.truncateThreshold = 0;
1515

1616
describe('agentResolver', () => {
1717
const projectDir = join('test', 'nuts', 'agents', 'agentsProject');
18+
const projectDir64 = join('test', 'nuts', 'agents', 'agentsProject64');
1819
const sourceDir = join(projectDir, 'force-app', 'main', 'default');
20+
const sourceDir64 = join(projectDir64, 'force-app', 'main', 'default');
1921
const allAgentMetadata = ['Bot', 'GenAiPlanner', 'GenAiPlugin', 'GenAiFunction'];
22+
const allAgentMetadata64 = ['Bot', 'GenAiPlannerBundle', 'GenAiPlugin', 'GenAiFunction'];
2023
const $$ = instantiateContext();
2124
const testOrg = new MockTestOrgData();
2225
let connection: Connection;
@@ -32,39 +35,75 @@ describe('agentResolver', () => {
3235
restoreContext($$);
3336
});
3437

35-
it('should return all top level agent metadata for wildcard and connection', async () => {
36-
const agentPseudoConfig = { botName: '*', connection };
37-
expect(await resolveAgentMdEntries(agentPseudoConfig)).to.deep.equal(allAgentMetadata);
38-
});
38+
describe('apiVersion 63.0 and lower', () => {
39+
it('should return all top level agent metadata for wildcard and connection', async () => {
40+
const agentPseudoConfig = { botName: '*', connection };
41+
expect(await resolveAgentMdEntries(agentPseudoConfig)).to.deep.equal(allAgentMetadata);
42+
});
3943

40-
it('should return all top level agent metadata for wildcard and directory', async () => {
41-
const agentPseudoConfig = { botName: '*', directoryPaths: [sourceDir] };
42-
expect(await resolveAgentMdEntries(agentPseudoConfig)).to.deep.equal(allAgentMetadata);
43-
});
44+
it('should return all top level agent metadata for wildcard and directory', async () => {
45+
const agentPseudoConfig = { botName: '*', directoryPaths: [sourceDir] };
46+
expect(await resolveAgentMdEntries(agentPseudoConfig)).to.deep.equal(allAgentMetadata);
47+
});
4448

45-
// Tests correct resolution when Bot API name does not match GenAiPlanner API name.
46-
it('should return metadata for internal agent and directory', async () => {
47-
const agentPseudoConfig = { botName: 'Copilot_for_Salesforce', directoryPaths: [sourceDir] };
48-
const expectedAgentMdEntries = ['Bot:Copilot_for_Salesforce', 'GenAiPlanner:EmployeeCopilotPlanner'];
49-
expect(await resolveAgentMdEntries(agentPseudoConfig)).to.deep.equal(expectedAgentMdEntries);
50-
});
49+
// Tests correct resolution when Bot API name does not match GenAiPlanner API name.
50+
it('should return metadata for internal agent and directory', async () => {
51+
const agentPseudoConfig = { botName: 'Copilot_for_Salesforce', directoryPaths: [sourceDir] };
52+
const expectedAgentMdEntries = ['Bot:Copilot_for_Salesforce', 'GenAiPlanner:EmployeeCopilotPlanner'];
53+
expect(await resolveAgentMdEntries(agentPseudoConfig)).to.deep.equal(expectedAgentMdEntries);
54+
});
55+
56+
it('should return metadata for agent (no plugins or functions) and directory', async () => {
57+
const agentPseudoConfig = { botName: 'My_Macys', directoryPaths: [sourceDir] };
58+
const expectedAgentMdEntries = ['Bot:My_Macys', 'GenAiPlanner:My_Macys'];
59+
expect(await resolveAgentMdEntries(agentPseudoConfig)).to.deep.equal(expectedAgentMdEntries);
60+
});
5161

52-
it('should return metadata for agent (no plugins or functions) and directory', async () => {
53-
const agentPseudoConfig = { botName: 'My_Macys', directoryPaths: [sourceDir] };
54-
const expectedAgentMdEntries = ['Bot:My_Macys', 'GenAiPlanner:My_Macys'];
55-
expect(await resolveAgentMdEntries(agentPseudoConfig)).to.deep.equal(expectedAgentMdEntries);
62+
it('should return metadata for agent (with plugins and functions) and directory', async () => {
63+
const agentPseudoConfig = { botName: 'The_Campus_Assistant', directoryPaths: [sourceDir] };
64+
const expectedAgentMdEntries = [
65+
'Bot:The_Campus_Assistant',
66+
'GenAiPlanner:The_Campus_Assistant',
67+
'GenAiPlugin:p_16jQP0000000PG9_Climbing_Routes_Information',
68+
'GenAiPlugin:p_16jQP0000000PG9_Gym_Hours_and_Schedule',
69+
'GenAiPlugin:p_16jQP0000000PG9_Membership_Plans',
70+
'GenAiFunction:CustomKnowledgeAction_1738277095539',
71+
];
72+
expect(await resolveAgentMdEntries(agentPseudoConfig)).to.deep.equal(expectedAgentMdEntries);
73+
});
5674
});
5775

58-
it('should return metadata for agent (with plugins and functions) and directory', async () => {
59-
const agentPseudoConfig = { botName: 'The_Campus_Assistant', directoryPaths: [sourceDir] };
60-
const expectedAgentMdEntries = [
61-
'Bot:The_Campus_Assistant',
62-
'GenAiPlanner:The_Campus_Assistant',
63-
'GenAiPlugin:p_16jQP0000000PG9_Climbing_Routes_Information',
64-
'GenAiPlugin:p_16jQP0000000PG9_Gym_Hours_and_Schedule',
65-
'GenAiPlugin:p_16jQP0000000PG9_Membership_Plans',
66-
'GenAiFunction:CustomKnowledgeAction_1738277095539',
67-
];
68-
expect(await resolveAgentMdEntries(agentPseudoConfig)).to.deep.equal(expectedAgentMdEntries);
76+
describe('apiVersion 64.0 and higher', () => {
77+
it('should return all top level agent metadata for wildcard and connection', async () => {
78+
connection.setApiVersion('64.0');
79+
const agentPseudoConfig = { botName: '*', connection };
80+
expect(await resolveAgentMdEntries(agentPseudoConfig)).to.deep.equal(allAgentMetadata64);
81+
});
82+
83+
// Tests correct resolution when Bot API name does not match GenAiPlannerBundle API name.
84+
it('should return metadata for internal agent and directory', async () => {
85+
const agentPseudoConfig = { botName: 'Copilot_for_Salesforce', directoryPaths: [sourceDir64] };
86+
const expectedAgentMdEntries = ['Bot:Copilot_for_Salesforce', 'GenAiPlannerBundle:EmployeeCopilotPlanner'];
87+
expect(await resolveAgentMdEntries(agentPseudoConfig)).to.deep.equal(expectedAgentMdEntries);
88+
});
89+
90+
it('should return metadata for agent (no plugins or functions) and directory', async () => {
91+
const agentPseudoConfig = { botName: 'My_Macys', directoryPaths: [sourceDir64] };
92+
const expectedAgentMdEntries = ['Bot:My_Macys', 'GenAiPlannerBundle:My_Macys'];
93+
expect(await resolveAgentMdEntries(agentPseudoConfig)).to.deep.equal(expectedAgentMdEntries);
94+
});
95+
96+
it('should return metadata for agent (with plugins and functions) and directory', async () => {
97+
const agentPseudoConfig = { botName: 'The_Campus_Assistant', directoryPaths: [sourceDir64] };
98+
const expectedAgentMdEntries = [
99+
'Bot:The_Campus_Assistant',
100+
'GenAiPlannerBundle:The_Campus_Assistant',
101+
'GenAiPlugin:p_16jQP0000000PG9_Climbing_Routes_Information',
102+
'GenAiPlugin:p_16jQP0000000PG9_Gym_Hours_and_Schedule',
103+
'GenAiPlugin:p_16jQP0000000PG9_Membership_Plans',
104+
'GenAiFunction:CustomKnowledgeAction_1738277095539',
105+
];
106+
expect(await resolveAgentMdEntries(agentPseudoConfig)).to.deep.equal(expectedAgentMdEntries);
107+
});
69108
});
70109
});

0 commit comments

Comments
 (0)