Skip to content

Commit f4794fd

Browse files
chore: use real apis
1 parent 59c5514 commit f4794fd

File tree

3 files changed

+141
-84
lines changed

3 files changed

+141
-84
lines changed

messages/agent.preview.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ Name of the linked client app to use for the agent connection.
3030

3131
Directory where conversation transcripts are saved.
3232

33-
# flags.mock-actions.summary
33+
# flags.use-live-actions.summary
3434

35-
quick summary here
35+
When true, will use real actions in the org, when false (default) will use AI to mock actions
3636

3737
# flags.apex-debug.summary
3838

src/commands/agent/preview.ts

Lines changed: 75 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { resolve, join } from 'node:path';
1818
import * as path from 'node:path';
1919
import { globSync } from 'glob';
2020
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
21-
import { AuthInfo, Connection, Messages, SfError, SfProject } from '@salesforce/core';
21+
import { AuthInfo, Connection, Lifecycle, Messages, SfError } from '@salesforce/core';
2222
import React from 'react';
2323
import { render } from 'ink';
2424
import { env } from '@salesforce/kit';
@@ -46,18 +46,18 @@ type Choice<Value> = {
4646
};
4747

4848
enum AgentSource {
49-
ORG = 'org',
50-
LOCAL = 'local',
49+
PUBLISHED = 'published',
50+
SCRIPT = 'script',
5151
}
5252

53-
type LocalAgent = { DeveloperName: string; source: AgentSource.LOCAL; path: string };
54-
type OrgAgent = {
53+
type ScriptAgent = { DeveloperName: string; source: AgentSource.SCRIPT; path: string };
54+
type PublishedAgent = {
5555
Id: string;
5656
DeveloperName: string;
57-
source: AgentSource.ORG;
57+
source: AgentSource.PUBLISHED;
5858
};
5959

60-
type AgentValue = LocalAgent | OrgAgent;
60+
type AgentValue = ScriptAgent | PublishedAgent;
6161

6262
// https://developer.salesforce.com/docs/einstein/genai/guide/agent-api-get-started.html#prerequisites
6363
export const UNSUPPORTED_AGENTS = ['Copilot_for_Salesforce'];
@@ -93,60 +93,68 @@ export default class AgentPreview extends SfCommand<AgentPreviewResult> {
9393
summary: messages.getMessage('flags.apex-debug.summary'),
9494
char: 'x',
9595
}),
96-
'mock-actions': Flags.boolean({
97-
summary: messages.getMessage('flags.mock-actions.summary'),
98-
dependsOn: ['authoring-bundle'],
96+
'use-live-actions': Flags.boolean({
97+
summary: messages.getMessage('flags.use-live-actions.summary'),
98+
default: false,
9999
}),
100100
};
101101

102102
public async run(): Promise<AgentPreviewResult> {
103+
// STAGES OF PREVIEW
104+
// get user's agent selection either from flags, or interaction
105+
// if .agent selected, use the AgentSimulate class to preview
106+
// if published agent, use AgentPreview for preview
107+
// based on agent, differing auth mechanisms required
103108
const { flags } = await this.parse(AgentPreview);
104109

105-
const { 'api-name': apiNameFlag } = flags;
110+
const { 'api-name': apiNameFlag, 'use-live-actions': useLiveActions } = flags;
106111
const conn = flags['target-org'].getConnection(flags['api-version']);
107-
const agentsQuery = await conn.query<AgentData>(
108-
'SELECT Id, DeveloperName, (SELECT Status FROM BotVersions) FROM BotDefinition WHERE IsDeleted = false'
109-
);
110-
111-
if (agentsQuery.totalSize === 0) throw new SfError('No Agents found in the org');
112112

113-
const agentsInOrg = agentsQuery.records;
113+
const agentsInOrg = (
114+
await conn.query<AgentData>(
115+
'SELECT Id, DeveloperName, (SELECT Status FROM BotVersions) FROM BotDefinition WHERE IsDeleted = false'
116+
)
117+
).records;
114118

115119
let selectedAgent: AgentValue;
116120

117121
if (flags['authoring-bundle']) {
122+
// user specified --authoring-bundle, we'll find the script and use it
118123
const bundlePath = findAuthoringBundle(this.project!.getPath(), flags['authoring-bundle']);
119124
if (!bundlePath) {
120125
throw new SfError(`Could not find authoring bundle for ${flags['authoring-bundle']}`);
121126
}
122127
selectedAgent = {
123128
DeveloperName: flags['authoring-bundle'],
124-
source: AgentSource.LOCAL,
129+
source: AgentSource.SCRIPT,
125130
path: join(bundlePath, `${flags['authoring-bundle']}.agent`),
126131
};
127132
} else if (apiNameFlag) {
133+
// user specified --api-name, it should be in the list of agents from the org
128134
const agent = agentsInOrg.find((a) => a.DeveloperName === apiNameFlag);
129135
if (!agent) throw new Error(`No valid Agents were found with the Api Name ${apiNameFlag}.`);
130136
validateAgent(agent);
131137
selectedAgent = {
132138
Id: agent.Id,
133139
DeveloperName: agent.DeveloperName,
134-
source: AgentSource.ORG,
140+
source: AgentSource.PUBLISHED,
135141
};
136142
if (!selectedAgent) throw new Error(`No valid Agents were found with the Api Name ${apiNameFlag}.`);
137143
} else {
138144
selectedAgent = await select({
139145
message: 'Select an agent',
140-
choices: getAgentChoices(agentsInOrg, this.project!),
146+
choices: this.getAgentChoices(agentsInOrg),
141147
});
142148
}
149+
150+
// we have the selected agent, create the appropriate connection
143151
const authInfo = await AuthInfo.create({
144152
username: flags['target-org'].getUsername(),
145153
});
146154
// Get client app - check flag first, then auth file, then env var
147155
let clientApp = flags['client-app'];
148156

149-
if (!clientApp && selectedAgent?.source === AgentSource.ORG) {
157+
if (!clientApp && selectedAgent?.source === AgentSource.PUBLISHED) {
150158
const clientApps = getClientAppsFromAuth(authInfo);
151159

152160
if (clientApps.length === 1) {
@@ -157,13 +165,18 @@ export default class AgentPreview extends SfCommand<AgentPreviewResult> {
157165
choices: clientApps.map((app) => ({ value: app, name: app })),
158166
});
159167
} else {
160-
// at this point we should throw an error
161168
throw new SfError('No client app found.');
162169
}
163170
}
164171

172+
if (useLiveActions && selectedAgent.source === AgentSource.PUBLISHED) {
173+
void Lifecycle.getInstance().emitWarning(
174+
'Published agents will always use real actions in your org, specifying --use-live-actions and selecting a published agent has no effect'
175+
);
176+
}
177+
165178
const jwtConn =
166-
selectedAgent?.source === AgentSource.ORG
179+
selectedAgent?.source === AgentSource.PUBLISHED
167180
? await Connection.create({
168181
authInfo,
169182
clientApp,
@@ -173,9 +186,9 @@ export default class AgentPreview extends SfCommand<AgentPreviewResult> {
173186
const outputDir = await resolveOutputDir(flags['output-dir'], flags['apex-debug']);
174187
// Both classes share the same interface for the methods we need
175188
const agentPreview =
176-
selectedAgent.source === AgentSource.ORG
189+
selectedAgent.source === AgentSource.PUBLISHED
177190
? new Preview(jwtConn, selectedAgent.Id)
178-
: new AgentSimulate(jwtConn, selectedAgent.path, flags['mock-actions'] ?? false);
191+
: new AgentSimulate(jwtConn, selectedAgent.path, useLiveActions);
179192

180193
agentPreview.toggleApexDebugMode(flags['apex-debug']);
181194

@@ -185,11 +198,48 @@ export default class AgentPreview extends SfCommand<AgentPreviewResult> {
185198
agent: agentPreview,
186199
name: selectedAgent.DeveloperName,
187200
outputDir,
201+
isLocalAgent: selectedAgent.source === AgentSource.SCRIPT,
188202
}),
189203
{ exitOnCtrlC: false }
190204
);
191205
await instance.waitUntilExit();
192206
}
207+
208+
private getAgentChoices(agents: AgentData[]): Array<Choice<AgentValue>> {
209+
const choices: Array<Choice<AgentValue>> = [];
210+
211+
// Add org agents
212+
for (const agent of agents) {
213+
if (agentIsInactive(agent) || agentIsUnsupported(agent.DeveloperName)) {
214+
continue;
215+
}
216+
217+
choices.push({
218+
name: `${agent.DeveloperName} (Published)`,
219+
value: {
220+
Id: agent.Id,
221+
DeveloperName: agent.DeveloperName,
222+
source: AgentSource.PUBLISHED,
223+
},
224+
});
225+
}
226+
227+
// Add local agents from .agent files
228+
const localAgentPaths = globSync('**/*.agent', { cwd: this.project!.getPath() });
229+
for (const agentPath of localAgentPaths) {
230+
const agentName = path.basename(agentPath, '.agent');
231+
choices.push({
232+
name: `${agentName} (Agent Script)`,
233+
value: {
234+
DeveloperName: agentName,
235+
source: AgentSource.SCRIPT,
236+
path: path.join(this.project!.getPath(), agentPath),
237+
},
238+
});
239+
}
240+
241+
return choices;
242+
}
193243
}
194244

195245
export const agentIsUnsupported = (devName: string): boolean => UNSUPPORTED_AGENTS.includes(devName);
@@ -213,42 +263,6 @@ export const validateAgent = (agent: AgentData): boolean => {
213263
return true;
214264
};
215265

216-
export const getAgentChoices = (agents: AgentData[], project: SfProject): Array<Choice<AgentValue>> => {
217-
const choices: Array<Choice<AgentValue>> = [];
218-
219-
// Add org agents
220-
for (const agent of agents) {
221-
if (agentIsInactive(agent) || agentIsUnsupported(agent.DeveloperName)) {
222-
continue;
223-
}
224-
225-
choices.push({
226-
name: `${agent.DeveloperName} (org)`,
227-
value: {
228-
Id: agent.Id,
229-
DeveloperName: agent.DeveloperName,
230-
source: AgentSource.ORG,
231-
},
232-
});
233-
}
234-
235-
// Add local agents from .agent files
236-
const localAgentPaths = globSync('**/*.agent', { cwd: project.getPath() });
237-
for (const agentPath of localAgentPaths) {
238-
const agentName = path.basename(agentPath, '.agent');
239-
choices.push({
240-
name: `${agentName} (local)`,
241-
value: {
242-
DeveloperName: agentName,
243-
source: AgentSource.LOCAL,
244-
path: path.join(project.getPath(), agentPath),
245-
},
246-
});
247-
}
248-
249-
return choices;
250-
};
251-
252266
export const getClientAppsFromAuth = (authInfo: AuthInfo): string[] =>
253267
Object.keys(authInfo.getFields().clientApps ?? {});
254268

src/components/agent-preview-react.tsx

Lines changed: 64 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import * as process from 'node:process';
2020
import React from 'react';
2121
import { Box, Text, useInput } from 'ink';
2222
import TextInput from 'ink-text-input';
23-
import { Connection, SfError } from '@salesforce/core';
23+
import { Connection, SfError, Lifecycle } from '@salesforce/core';
2424
import { AgentPreviewBase, AgentPreviewSendResponse, writeDebugLog } from '@salesforce/agents';
2525
import { sleep } from '@salesforce/kit';
2626

@@ -48,10 +48,6 @@ function Typing(): React.ReactNode {
4848
);
4949
}
5050

51-
// Split the content on newlines, then find the longest array element
52-
const calculateWidth = (content: string): number =>
53-
content.split('\n').reduce((acc, line) => Math.max(acc, line.length), 0) + 4;
54-
5551
const saveTranscriptsToFile = (
5652
outputDir: string,
5753
messages: Array<{ timestamp: Date; role: string; content: string }>,
@@ -79,6 +75,7 @@ export function AgentPreviewReact(props: {
7975
readonly agent: AgentPreviewBase;
8076
readonly name: string;
8177
readonly outputDir: string | undefined;
78+
readonly isLocalAgent: boolean;
8279
}): React.ReactNode {
8380
const [messages, setMessages] = React.useState<Array<{ timestamp: Date; role: string; content: string }>>([]);
8481
const [header, setHeader] = React.useState('Starting session...');
@@ -93,7 +90,7 @@ export function AgentPreviewReact(props: {
9390
const [responses, setResponses] = React.useState<AgentPreviewSendResponse[]>([]);
9491
const [apexDebugLogs, setApexDebugLogs] = React.useState<string[]>([]);
9592

96-
const { connection, agent, name, outputDir } = props;
93+
const { connection, agent, name, outputDir, isLocalAgent } = props;
9794

9895
useInput((input, key) => {
9996
if (key.escape) {
@@ -121,6 +118,29 @@ export function AgentPreviewReact(props: {
121118
}, [sessionEnded]);
122119

123120
React.useEffect(() => {
121+
// Set up event listeners for agent compilation and simulation events
122+
const lifecycle = Lifecycle.getInstance();
123+
124+
const handleCompilingEvent = (): Promise<void> => {
125+
setHeader('Compiling agent...');
126+
return Promise.resolve();
127+
};
128+
129+
const handleSimulationStartingEvent = (): Promise<void> => {
130+
setHeader('Starting session...');
131+
return Promise.resolve();
132+
};
133+
134+
const handleSessionStartedEvent = (): Promise<void> => {
135+
setHeader(`New session started with "${props.name}"`);
136+
return Promise.resolve();
137+
};
138+
139+
// Listen for the events
140+
lifecycle.on('agents:compiling', handleCompilingEvent);
141+
lifecycle.on('agents:simulation-starting', handleSimulationStartingEvent);
142+
lifecycle.on('agents:session-started', handleSessionStartedEvent);
143+
124144
const startSession = async (): Promise<void> => {
125145
try {
126146
const session = await agent.start();
@@ -132,7 +152,17 @@ export function AgentPreviewReact(props: {
132152
const dateForDir = new Date().toISOString().replace(/:/g, '-').split('.')[0];
133153
setTempDir(path.join(outputDir, `${dateForDir}--${session.sessionId}`));
134154
}
135-
setMessages([{ role: name, content: session.messages[0].message, timestamp: new Date() }]);
155+
// Add disclaimer for local agents before the agent's first message
156+
const initialMessages = [];
157+
if (isLocalAgent) {
158+
initialMessages.push({
159+
role: 'system',
160+
content: 'Agent preview does not provide strict adherence to connection endpoint configuration and escalation is not supported.\n\nTo test escalation, publish your agent then use the desired connection endpoint (e.g., Web Page, SMS, etc).',
161+
timestamp: new Date(),
162+
});
163+
}
164+
initialMessages.push({ role: name, content: session.messages[0].message, timestamp: new Date() });
165+
setMessages(initialMessages);
136166
} catch (e) {
137167
const sfError = SfError.wrap(e);
138168
setIsTyping(false);
@@ -143,7 +173,7 @@ export function AgentPreviewReact(props: {
143173
};
144174

145175
void startSession();
146-
}, []);
176+
}, [agent, name, outputDir, props.name, isLocalAgent]);
147177

148178
React.useEffect(() => {
149179
saveTranscriptsToFile(tempDir, messages, responses);
@@ -171,19 +201,32 @@ export function AgentPreviewReact(props: {
171201
alignItems={role === 'user' ? 'flex-end' : 'flex-start'}
172202
flexDirection="column"
173203
>
174-
<Box flexDirection="row" columnGap={1}>
175-
<Text>{role === 'user' ? 'You' : role}</Text>
176-
<Text color="grey">{ts.toLocaleString()}</Text>
177-
</Box>
178-
<Box
179-
// Use 70% of the terminal width, or the width of a single line of content, whichever is smaller
180-
width={Math.min(process.stdout.columns * 0.7, calculateWidth(content))}
181-
borderStyle="round"
182-
paddingLeft={1}
183-
paddingRight={1}
184-
>
185-
<Text>{content}</Text>
186-
</Box>
204+
{role === 'system' ? (
205+
<Box
206+
width={process.stdout.columns}
207+
borderStyle="round"
208+
borderColor="yellow"
209+
paddingLeft={1}
210+
paddingRight={1}
211+
marginBottom={1}
212+
>
213+
<Text>{content}</Text>
214+
</Box>
215+
) : (
216+
<>
217+
<Box flexDirection="row" columnGap={1}>
218+
<Text>{role === 'user' ? 'You' : role}</Text>
219+
<Text color="grey">{ts.toLocaleString()}</Text>
220+
</Box>
221+
<Box
222+
borderStyle="round"
223+
paddingLeft={1}
224+
paddingRight={1}
225+
>
226+
<Text>{content}</Text>
227+
</Box>
228+
</>
229+
)}
187230
</Box>
188231
))}
189232
</Box>

0 commit comments

Comments
 (0)