Skip to content

Commit 7af747d

Browse files
Merge pull request #249 from salesforcecli/wr/agentSimulate
Wr/agent simulate @W-20017433@
2 parents 46560d5 + 9dc8d4e commit 7af747d

File tree

7 files changed

+984
-889
lines changed

7 files changed

+984
-889
lines changed

command-snapshot.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@
8888
"client-app",
8989
"flags-dir",
9090
"output-dir",
91-
"target-org"
91+
"target-org",
92+
"use-live-actions"
9293
],
9394
"plugin": "@salesforce/plugin-agent"
9495
},

messages/agent.preview.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ API name of the agent you want to interact with.
2020

2121
# flags.authoring-bundle.summary
2222

23-
Preview a next-gen agent by specifying the API name of the authoring bundle metadata component that implements it.
23+
Preview a next-gen agent by specifying the API name of the authoring bundle metadata component that implements it.
2424

2525
# flags.client-app.summary
2626

@@ -30,6 +30,10 @@ Name of the linked client app to use for the agent connection.
3030

3131
Directory where conversation transcripts are saved.
3232

33+
# flags.use-live-actions.summary
34+
35+
Use real actions in the org; if not specified, preview uses AI to mock actions.
36+
3337
# flags.apex-debug.summary
3438

3539
Enable Apex debug logging during the agent preview conversation.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
"@salesforce/sf-plugins-core": "^12.2.4",
1616
"@salesforce/source-deploy-retrieve": "^12.25.0",
1717
"@salesforce/types": "^1.4.0",
18+
"@types/glob": "^9.0.0",
1819
"ansis": "^3.3.2",
1920
"fast-xml-parser": "^4.5.1",
21+
"glob": "^11.0.3",
2022
"ink": "5.0.1",
2123
"ink-text-input": "^6.0.0",
2224
"inquirer-autocomplete-standalone": "^0.8.1",

src/commands/agent/preview.ts

Lines changed: 130 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,23 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { resolve, join } from 'node:path';
18-
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
19-
import { AuthInfo, Connection, Messages, SfError } from '@salesforce/core';
17+
import * as path from 'node:path';
18+
import { join, resolve } from 'node:path';
19+
import { globSync } from 'glob';
20+
import { Flags, SfCommand } from '@salesforce/sf-plugins-core';
21+
import { AuthInfo, Connection, Lifecycle, Messages, SfError } from '@salesforce/core';
2022
import React from 'react';
2123
import { render } from 'ink';
2224
import { env } from '@salesforce/kit';
23-
import { AgentPreview as Preview } from '@salesforce/agents';
24-
import { select, confirm, input } from '@inquirer/prompts';
25+
import {
26+
AgentPreview as Preview,
27+
AgentSimulate,
28+
AgentSource,
29+
findAuthoringBundle,
30+
PublishedAgent,
31+
ScriptAgent,
32+
} from '@salesforce/agents';
33+
import { confirm, input, select } from '@inquirer/prompts';
2534
import { AgentPreviewReact } from '../../components/agent-preview-react.js';
2635

2736
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
@@ -43,11 +52,6 @@ type Choice<Value> = {
4352
disabled?: boolean | string;
4453
};
4554

46-
type AgentValue = {
47-
Id: string;
48-
DeveloperName: string;
49-
};
50-
5155
// https://developer.salesforce.com/docs/einstein/genai/guide/agent-api-get-started.html#prerequisites
5256
export const UNSUPPORTED_AGENTS = ['Copilot_for_Salesforce'];
5357

@@ -82,73 +86,153 @@ export default class AgentPreview extends SfCommand<AgentPreviewResult> {
8286
summary: messages.getMessage('flags.apex-debug.summary'),
8387
char: 'x',
8488
}),
89+
'use-live-actions': Flags.boolean({
90+
summary: messages.getMessage('flags.use-live-actions.summary'),
91+
default: false,
92+
}),
8593
};
8694

8795
public async run(): Promise<AgentPreviewResult> {
96+
// STAGES OF PREVIEW
97+
// get user's agent selection either from flags, or interaction
98+
// if .agent selected, use the AgentSimulate class to preview
99+
// if published agent, use AgentPreview for preview
100+
// based on agent, differing auth mechanisms required
88101
const { flags } = await this.parse(AgentPreview);
89102

90-
const { 'api-name': apiNameFlag } = flags;
103+
const { 'api-name': apiNameFlag, 'use-live-actions': useLiveActions } = flags;
91104
const conn = flags['target-org'].getConnection(flags['api-version']);
92105

93-
const authInfo = await AuthInfo.create({
94-
username: flags['target-org'].getUsername(),
95-
});
96-
if (!(flags['client-app'] ?? env.getString('SF_DEMO_AGENT_CLIENT_APP'))) {
97-
throw new SfError('SF_DEMO_AGENT_CLIENT_APP is unset!');
98-
}
99-
100-
const jwtConn = await Connection.create({
101-
authInfo,
102-
clientApp: env.getString('SF_DEMO_AGENT_CLIENT_APP') ?? flags['client-app'],
103-
});
106+
const agentsInOrg = (
107+
await conn.query<AgentData>(
108+
'SELECT Id, DeveloperName, (SELECT Status FROM BotVersions) FROM BotDefinition WHERE IsDeleted = false'
109+
)
110+
).records;
104111

105-
const agentsQuery = await conn.query<AgentData>(
106-
'SELECT Id, DeveloperName, (SELECT Status FROM BotVersions) FROM BotDefinition WHERE IsDeleted = false'
107-
);
108-
109-
if (agentsQuery.totalSize === 0) throw new SfError('No Agents found in the org');
110-
111-
const agentsInOrg = agentsQuery.records;
112-
113-
let selectedAgent;
112+
let selectedAgent: ScriptAgent | PublishedAgent;
114113

115114
if (flags['authoring-bundle']) {
116-
const envAgentName = env.getString('SF_DEMO_AGENT');
117-
const agent = agentsQuery.records.find((a) => a.DeveloperName === envAgentName);
115+
// user specified --authoring-bundle, we'll find the script and use it
116+
const bundlePath = findAuthoringBundle(this.project!.getPath(), flags['authoring-bundle']);
117+
if (!bundlePath) {
118+
throw new SfError(`Could not find authoring bundle for ${flags['authoring-bundle']}`);
119+
}
118120
selectedAgent = {
119-
Id:
120-
agent?.Id ??
121-
`Couldn't find an agent in ${agentsQuery.records.map((a) => a.DeveloperName).join(', ')} matching ${
122-
envAgentName ?? '!SF_DEMO_AGENT is unset!'
123-
}`,
124121
DeveloperName: flags['authoring-bundle'],
122+
source: AgentSource.SCRIPT,
123+
path: join(bundlePath, `${flags['authoring-bundle']}.agent`),
125124
};
126125
} else if (apiNameFlag) {
127-
selectedAgent = agentsInOrg.find((agent) => agent.DeveloperName === apiNameFlag);
126+
// user specified --api-name, it should be in the list of agents from the org
127+
const agent = agentsInOrg.find((a) => a.DeveloperName === apiNameFlag);
128+
if (!agent) throw new Error(`No valid Agents were found with the Api Name ${apiNameFlag}.`);
129+
validateAgent(agent);
130+
selectedAgent = {
131+
Id: agent.Id,
132+
DeveloperName: agent.DeveloperName,
133+
source: AgentSource.PUBLISHED,
134+
};
128135
if (!selectedAgent) throw new Error(`No valid Agents were found with the Api Name ${apiNameFlag}.`);
129-
validateAgent(selectedAgent);
130136
} else {
131-
selectedAgent = await select({
137+
selectedAgent = await select<ScriptAgent | PublishedAgent>({
132138
message: 'Select an agent',
133-
choices: getAgentChoices(agentsInOrg),
139+
choices: this.getAgentChoices(agentsInOrg),
134140
});
135141
}
136142

143+
// we have the selected agent, create the appropriate connection
144+
const authInfo = await AuthInfo.create({
145+
username: flags['target-org'].getUsername(),
146+
});
147+
// Get client app - check flag first, then auth file, then env var
148+
let clientApp = flags['client-app'];
149+
150+
if (!clientApp && selectedAgent?.source === AgentSource.PUBLISHED) {
151+
const clientApps = getClientAppsFromAuth(authInfo);
152+
153+
if (clientApps.length === 1) {
154+
clientApp = clientApps[0];
155+
} else if (clientApps.length > 1) {
156+
clientApp = await select({
157+
message: 'Select a client app',
158+
choices: clientApps.map((app) => ({ value: app, name: app })),
159+
});
160+
} else {
161+
throw new SfError('No client app found.');
162+
}
163+
}
164+
165+
if (useLiveActions && selectedAgent.source === AgentSource.PUBLISHED) {
166+
void Lifecycle.getInstance().emitWarning(
167+
'Published agents will always use real actions in your org, specifying --use-live-actions and selecting a published agent has no effect'
168+
);
169+
}
170+
171+
const jwtConn =
172+
selectedAgent?.source === AgentSource.PUBLISHED
173+
? await Connection.create({
174+
authInfo,
175+
clientApp,
176+
})
177+
: await Connection.create({ authInfo });
178+
137179
const outputDir = await resolveOutputDir(flags['output-dir'], flags['apex-debug']);
138-
const agentPreview = new Preview(jwtConn, selectedAgent.Id);
139-
agentPreview.toggleApexDebugMode(flags['apex-debug']);
180+
// Both classes share the same interface for the methods we need
181+
const agentPreview =
182+
selectedAgent.source === AgentSource.PUBLISHED
183+
? new Preview(jwtConn, selectedAgent.Id)
184+
: new AgentSimulate(jwtConn, selectedAgent.path, useLiveActions);
185+
186+
agentPreview.setApexDebugMode(flags['apex-debug']);
140187

141188
const instance = render(
142189
React.createElement(AgentPreviewReact, {
143190
connection: conn,
144191
agent: agentPreview,
145192
name: selectedAgent.DeveloperName,
146193
outputDir,
194+
isLocalAgent: selectedAgent.source === AgentSource.SCRIPT,
147195
}),
148196
{ exitOnCtrlC: false }
149197
);
150198
await instance.waitUntilExit();
151199
}
200+
201+
private getAgentChoices(agents: AgentData[]): Array<Choice<ScriptAgent | PublishedAgent>> {
202+
const choices: Array<Choice<ScriptAgent | PublishedAgent>> = [];
203+
204+
// Add org agents
205+
for (const agent of agents) {
206+
if (agentIsInactive(agent) || agentIsUnsupported(agent.DeveloperName)) {
207+
continue;
208+
}
209+
210+
choices.push({
211+
name: `${agent.DeveloperName} (Published)`,
212+
value: {
213+
Id: agent.Id,
214+
DeveloperName: agent.DeveloperName,
215+
source: AgentSource.PUBLISHED,
216+
},
217+
});
218+
}
219+
220+
// Add local agents from .agent files
221+
const localAgentPaths = globSync('**/*.agent', { cwd: this.project!.getPath() });
222+
for (const agentPath of localAgentPaths) {
223+
const agentName = path.basename(agentPath, '.agent');
224+
choices.push({
225+
name: `${agentName} (Agent Script)`,
226+
value: {
227+
DeveloperName: agentName,
228+
source: AgentSource.SCRIPT,
229+
path: path.join(this.project!.getPath(), agentPath),
230+
},
231+
});
232+
}
233+
234+
return choices;
235+
}
152236
}
153237

154238
export const agentIsUnsupported = (devName: string): boolean => UNSUPPORTED_AGENTS.includes(devName);
@@ -172,22 +256,8 @@ export const validateAgent = (agent: AgentData): boolean => {
172256
return true;
173257
};
174258

175-
export const getAgentChoices = (agents: AgentData[]): Array<Choice<AgentValue>> =>
176-
agents.map((agent) => {
177-
let disabled: string | boolean = false;
178-
179-
if (agentIsInactive(agent)) disabled = '(Inactive)';
180-
if (agentIsUnsupported(agent.DeveloperName)) disabled = '(Not Supported)';
181-
182-
return {
183-
name: agent.DeveloperName,
184-
value: {
185-
Id: agent.Id,
186-
DeveloperName: agent.DeveloperName,
187-
},
188-
disabled,
189-
};
190-
});
259+
export const getClientAppsFromAuth = (authInfo: AuthInfo): string[] =>
260+
Object.keys(authInfo.getFields().clientApps ?? {});
191261

192262
export const resolveOutputDir = async (
193263
outputDir: string | undefined,

0 commit comments

Comments
 (0)