Skip to content

Commit 935645e

Browse files
W-18551485: update agentResolver to get all customized geAiPlugins from the org (#1567)
* fix: update agentResolver to get all customized geAiPlugins from the org * chore: bump salesforce/core version
1 parent 961942d commit 935645e

File tree

3 files changed

+159
-25
lines changed

3 files changed

+159
-25
lines changed

src/resolve/pseudoTypes/agentResolver.ts

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import { readFileSync } from 'node:fs';
99
import { join } from 'node:path';
1010
import { XMLParser } from 'fast-xml-parser';
11-
import { Connection, Logger, SfError, trimTo15 } from '@salesforce/core';
11+
import { Connection, Logger, SfError, validateSalesforceId } from '@salesforce/core';
1212
import type { BotVersion, GenAiPlanner, GenAiPlannerFunctionDef, GenAiPlugin } from '@salesforce/types/metadata';
1313
import { ensureArray } from '@salesforce/kit';
1414
import { RegistryAccess } from '../../registry';
@@ -107,32 +107,37 @@ const resolveAgentFromConnection = async (connection: Connection, botName: strin
107107
if (plannerId) {
108108
const plannerType = Number(connection.getApiVersion()) > 63.0 ? 'GenAiPlannerBundle' : 'GenAiPlanner';
109109
mdEntries.push(`${plannerType}:${botName}`);
110-
const plannerId15 = trimTo15(plannerId);
111-
// Query for the GenAiPlugins associated with the 15 char GenAiPlannerId
112-
const genAiPluginNames = (
113-
await connection.tooling.query<{ DeveloperName: string }>(
114-
`SELECT DeveloperName FROM GenAiPluginDefinition WHERE DeveloperName LIKE 'p_${plannerId15}%'`
110+
// Query the junction table to get all GenAiPlugins associated with this GenAiPlannerId
111+
const topicIdsByPlanner = (
112+
await connection.tooling.query<{ Plugin: string }>(
113+
`SELECT Plugin FROM GenAiPlannerFunctionDef WHERE PlannerId = '${plannerId}'`
115114
)
116115
).records;
117-
if (genAiPluginNames.length) {
118-
genAiPluginNames.map((r) => mdEntries.push(`GenAiPlugin:${r.DeveloperName}`));
116+
if (topicIdsByPlanner.length) {
117+
// Query the GenAiPluginDefinition table to get the DeveloperName for each GenAiPluginId
118+
const genAiPluginIds = `'${topicIdsByPlanner
119+
.map((record) => record.Plugin)
120+
.filter((pluginId) => validateSalesforceId(pluginId))
121+
.join("','")}'`;
122+
// This query returns customized plugins only
123+
const genAiPluginNames = (
124+
await connection.tooling.query<{ DeveloperName: string }>(
125+
`SELECT DeveloperName FROM GenAiPluginDefinition WHERE Id IN (${genAiPluginIds})`
126+
)
127+
).records;
128+
if (genAiPluginNames.length) {
129+
genAiPluginNames.map((r) => mdEntries.push(`GenAiPlugin:${r.DeveloperName}`));
130+
} else {
131+
getLogger().debug(
132+
`No GenAiPlugin metadata matches for plannerId: ${plannerId}. Reading the ${plannerType} metadata for plugins...`
133+
);
134+
await getPluginNamesFromPlanner(connection, botName, mdEntries);
135+
}
119136
} else {
120137
getLogger().debug(
121-
`No GenAiPlugin metadata matches for plannerId: ${plannerId15}. Reading the ${plannerType} metadata for plugins...`
138+
`No GenAiPlugin metadata matches for plannerId: ${plannerId}. Reading the ${plannerType} metadata for plugins...`
122139
);
123-
// read the planner metadata from the org
124-
// @ts-expect-error jsForce types don't know about GenAiPlanner yet
125-
const genAiPlannerMd = await connection.metadata.read<GenAiPlanner>(plannerType, botName);
126-
const genAiPlannerMdArr = ensureArray(genAiPlannerMd) as unknown as GenAiPlanner[];
127-
if (genAiPlannerMdArr?.length && genAiPlannerMdArr[0]?.genAiPlugins.length) {
128-
genAiPlannerMdArr[0].genAiPlugins.map((plugin) => {
129-
if (plugin.genAiPluginName?.length) {
130-
mdEntries.push(`GenAiPlugin:${plugin.genAiPluginName}`);
131-
}
132-
});
133-
} else {
134-
getLogger().debug(`No GenAiPlugin metadata found in planner file for API name: ${botName}`);
135-
}
140+
await getPluginNamesFromPlanner(connection, botName, mdEntries);
136141
}
137142
} else {
138143
getLogger().debug(`No GenAiPlanner metadata matches for Bot: ${botName}`);
@@ -290,3 +295,25 @@ const xmlToJson = <T>(path: string, parser: XMLParser): T => {
290295
if (!file) throw new SfError(`No metadata file found at ${path}`);
291296
return parser.parse(file) as T;
292297
};
298+
299+
// read the GenAiPlanner or GenAiPlannerBundle metadata from the org and add the GenAiPlugin names to the mdEntries array
300+
const getPluginNamesFromPlanner = async (
301+
connection: Connection,
302+
botName: string,
303+
mdEntries: string[]
304+
): Promise<void> => {
305+
const plannerType = Number(connection.getApiVersion()) > 63.0 ? 'GenAiPlannerBundle' : 'GenAiPlanner';
306+
// @ts-expect-error jsForce types don't know about GenAiPlanner yet
307+
const genAiPlannerMd = await connection.metadata.read<GenAiPlanner>(plannerType, botName);
308+
309+
const genAiPlannerMdArr = ensureArray(genAiPlannerMd) as unknown as GenAiPlanner[];
310+
if (genAiPlannerMdArr?.length && genAiPlannerMdArr[0]?.genAiPlugins.length) {
311+
genAiPlannerMdArr[0].genAiPlugins.map((plugin) => {
312+
if (plugin.genAiPluginName?.length) {
313+
mdEntries.push(`GenAiPlugin:${plugin.genAiPluginName}`);
314+
}
315+
});
316+
} else {
317+
getLogger().debug(`No GenAiPlugin metadata found in planner file for API name: ${botName}`);
318+
}
319+
};

test/collections/componentSetBuilder.test.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,7 @@ describe('ComponentSetBuilder', () => {
685685
describe('Agent pseudo type', () => {
686686
const genAiPlannerId = '16jSB000000H3JFYA0';
687687
const genAiPlannerId15 = '16jSB000000H3JF';
688+
const genAiPluginId = '179WJ0000004VI9YAM';
688689
const packageDir1 = path.resolve('force-app');
689690
const botComponent = {
690691
type: 'Bot',
@@ -740,10 +741,16 @@ describe('ComponentSetBuilder', () => {
740741

741742
it('should create ComponentSet from org connection and Agent developer name', async () => {
742743
const botName = botComponent.fullName;
744+
const plannerToPlugins = [{ Plugin: genAiPluginId }];
745+
const pluginDeveloperNames = [{ DeveloperName: genAiPluginComponent.fullName }];
746+
743747
const srq = `SELECT Id FROM GenAiPlannerDefinition WHERE DeveloperName = '${botName}'`;
744-
const query = `SELECT DeveloperName FROM GenAiPluginDefinition WHERE DeveloperName LIKE 'p_${genAiPlannerId15}%'`;
748+
const plannerToPluginsQuery = `SELECT Plugin FROM GenAiPlannerFunctionDef WHERE PlannerId = '${genAiPlannerId}'`;
749+
const genAiPluginNamesQuery = `SELECT DeveloperName FROM GenAiPluginDefinition WHERE Id IN ('${genAiPluginId}')`;
750+
745751
singleRecordQueryStub.withArgs(srq, { tooling: true }).resolves({ Id: genAiPlannerId });
746-
queryStub.withArgs(query).resolves({ records: [{ DeveloperName: genAiPluginComponent.fullName }] });
752+
queryStub.withArgs(plannerToPluginsQuery).resolves({ records: plannerToPlugins });
753+
queryStub.withArgs(genAiPluginNamesQuery).resolves({ records: pluginDeveloperNames });
747754

748755
const mdCompSet = new ComponentSet();
749756
mdCompSet.add(botComponent);
@@ -764,7 +771,7 @@ describe('ComponentSetBuilder', () => {
764771

765772
const compSet = await ComponentSetBuilder.build(options);
766773
expect(singleRecordQueryStub.callCount, 'Expected singleRecordQuery stub to be called').to.equal(1);
767-
expect(queryStub.callCount, 'Expected tooling query stub to be called').to.equal(1);
774+
expect(queryStub.callCount, 'Expected tooling query stub to be called').to.equal(2);
768775
expect(fromConnectionStub.callCount).to.equal(1);
769776
const fromConnectionArgs = fromConnectionStub.firstCall.args[0];
770777
const expectedMdTypes = ['Bot', 'GenAiPlanner', 'GenAiPlugin'];

test/nuts/agents/agentResolver.test.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,5 +105,105 @@ describe('agentResolver', () => {
105105
];
106106
expect(await resolveAgentMdEntries(agentPseudoConfig)).to.deep.equal(expectedAgentMdEntries);
107107
});
108+
109+
describe('with a connection', () => {
110+
const genAiPlannerId = '16jWJ000000275RYAQ';
111+
112+
const plannerIdQuery = "SELECT Id FROM GenAiPlannerDefinition WHERE DeveloperName = 'The_Campus_Assistant'";
113+
const plannerToPluginsQuery = `SELECT Plugin FROM GenAiPlannerFunctionDef WHERE PlannerId = '${genAiPlannerId}'`;
114+
let queryStub: sinon.SinonStub;
115+
let singleRecordQueryStub: sinon.SinonStub;
116+
117+
beforeEach(() => {
118+
connection.setApiVersion('64.0');
119+
singleRecordQueryStub = $$.SANDBOX.stub(connection, 'singleRecordQuery');
120+
singleRecordQueryStub.withArgs(plannerIdQuery, { tooling: true }).resolves({ Id: genAiPlannerId });
121+
queryStub = $$.SANDBOX.stub(connection.tooling, 'query');
122+
});
123+
124+
it('should return metadata for agent (with plugins) from the org', async () => {
125+
const pluginIds = ['179WJ0000004VI9YAM', '179WJ0000004VIAYA2', '179WJ0000004VIBYA2', '179WJ0000004VICYA2'];
126+
const plannerToPlugins = [
127+
{ Plugin: pluginIds[0] },
128+
{ Plugin: pluginIds[1] },
129+
{ Plugin: pluginIds[2] },
130+
{ Plugin: pluginIds[3] },
131+
];
132+
const pluginDeveloperNames = [
133+
{ DeveloperName: 'p_16jQP0000000PG9_Climbing_Routes_Information' },
134+
{ DeveloperName: 'p_16jQP0000000PG9_Gym_Hours_and_Schedule' },
135+
{ DeveloperName: 'p_16jQP0000000PG9_Membership_Plans' },
136+
{ DeveloperName: 'Topic_Goal' },
137+
];
138+
const genAiPluginNamesQuery = `SELECT DeveloperName FROM GenAiPluginDefinition WHERE Id IN ('${pluginIds.join(
139+
"','"
140+
)}')`;
141+
queryStub.withArgs(plannerToPluginsQuery).resolves({ records: plannerToPlugins });
142+
queryStub.withArgs(genAiPluginNamesQuery).resolves({ records: pluginDeveloperNames });
143+
144+
const agentPseudoConfig = { botName: 'The_Campus_Assistant', connection };
145+
const result = await resolveAgentMdEntries(agentPseudoConfig);
146+
147+
// Should include Bot, Planner, and only the non-p_plannerId plugins
148+
expect(result).to.deep.equal([
149+
'Bot:The_Campus_Assistant',
150+
'GenAiPlannerBundle:The_Campus_Assistant',
151+
'GenAiPlugin:p_16jQP0000000PG9_Climbing_Routes_Information',
152+
'GenAiPlugin:p_16jQP0000000PG9_Gym_Hours_and_Schedule',
153+
'GenAiPlugin:p_16jQP0000000PG9_Membership_Plans',
154+
'GenAiPlugin:Topic_Goal',
155+
]);
156+
});
157+
158+
it('should handle the case where the planner has no plugins', async () => {
159+
queryStub.withArgs(plannerToPluginsQuery).resolves({ records: [] });
160+
161+
const agentPseudoConfig = { botName: 'The_Campus_Assistant', connection };
162+
const result = await resolveAgentMdEntries(agentPseudoConfig);
163+
164+
// Should include Bot, Planner, and only the non-p_plannerId plugins
165+
expect(result).to.deep.equal(['Bot:The_Campus_Assistant', 'GenAiPlannerBundle:The_Campus_Assistant']);
166+
});
167+
168+
it('should handle the case where the planner has global plugins only', async () => {
169+
const pluginIds = ['someStandardPlugin'];
170+
const plannerToPlugins = [{ Plugin: pluginIds[0] }];
171+
const genAiPluginNamesQuery = `SELECT DeveloperName FROM GenAiPluginDefinition WHERE Id IN ('${pluginIds[0]}')`;
172+
queryStub.withArgs(plannerToPluginsQuery).resolves({ records: plannerToPlugins });
173+
queryStub.withArgs(genAiPluginNamesQuery).resolves({ records: [] });
174+
175+
const agentPseudoConfig = { botName: 'The_Campus_Assistant', connection };
176+
const result = await resolveAgentMdEntries(agentPseudoConfig);
177+
178+
// Should include Bot, Planner, and only the non-p_plannerId plugins
179+
expect(result).to.deep.equal(['Bot:The_Campus_Assistant', 'GenAiPlannerBundle:The_Campus_Assistant']);
180+
});
181+
182+
it('should list customized plugins only', async () => {
183+
// in this case, the planner has global plugins and customized plugins
184+
const pluginIds = ['179WJ0000004VI9YAM', '179WJ0000004VIAYA2', 'someStandardPlugin'];
185+
const plannerToPlugins = [{ Plugin: pluginIds[0] }, { Plugin: pluginIds[1] }, { Plugin: pluginIds[2] }];
186+
const pluginDeveloperNames = [
187+
{ DeveloperName: 'p_16jQP0000000PG9_Climbing_Routes_Information' },
188+
{ DeveloperName: 'p_16jQP0000000PG9_Gym_Hours_and_Schedule' },
189+
];
190+
const genAiPluginNamesQuery = `SELECT DeveloperName FROM GenAiPluginDefinition WHERE Id IN ('${pluginIds.join(
191+
"','"
192+
)}')`;
193+
queryStub.withArgs(plannerToPluginsQuery).resolves({ records: plannerToPlugins });
194+
queryStub.withArgs(genAiPluginNamesQuery).resolves({ records: pluginDeveloperNames });
195+
196+
const agentPseudoConfig = { botName: 'The_Campus_Assistant', connection };
197+
const result = await resolveAgentMdEntries(agentPseudoConfig);
198+
199+
// Should include Bot, Planner, and only the non-p_plannerId plugins
200+
expect(result).to.deep.equal([
201+
'Bot:The_Campus_Assistant',
202+
'GenAiPlannerBundle:The_Campus_Assistant',
203+
'GenAiPlugin:p_16jQP0000000PG9_Climbing_Routes_Information',
204+
'GenAiPlugin:p_16jQP0000000PG9_Gym_Hours_and_Schedule',
205+
]);
206+
});
207+
});
108208
});
109209
});

0 commit comments

Comments
 (0)