Skip to content

Commit 90cad63

Browse files
authored
Merge pull request #73 from salesforcecli/ew/agent-preview
W-17736371 agent preview
2 parents 9440f34 + d86df11 commit 90cad63

10 files changed

Lines changed: 3389 additions & 2473 deletions

File tree

README.md

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -347,35 +347,76 @@ _See code: [src/commands/agent/generate/test-spec.ts](https://github.com/salesfo
347347

348348
## `sf agent preview`
349349

350-
Interact with an active agent, as a user would, to preview responses
350+
Interact with an active agent to preview how the agent responds to your statements, questions, and commands (utterances).
351351

352352
```
353353
USAGE
354-
$ sf agent preview -o <value> -n <value> [--flags-dir <value>] [--api-version <value>]
354+
$ sf agent preview -o <value> -a <value> [--flags-dir <value>] [--api-version <value>] [-n <value>] [-d <value>]
355355
356356
FLAGS
357-
-n, --name=<value> (required) The name of the agent you want to preview
358-
-o, --target-org=<value> (required) Username or alias of the target org. Not required if the `target-org`
359-
configuration variable is already set.
360-
--api-version=<value> Override the api version used for api requests made by this command
357+
-a, --connected-app-user=<value> (required) Username or alias of the connected app user that's configured with
358+
JWT-based access tokens to the agent.
359+
-d, --output-dir=<value> Directory where conversation transcripts are saved.
360+
-n, --api-name=<value> API name of the agent you want to interact with.
361+
-o, --target-org=<value> (required) Username or alias of the target org. Not required if the `target-org`
362+
configuration variable is already set.
363+
--api-version=<value> Override the api version used for api requests made by this command
361364
362365
GLOBAL FLAGS
363366
--flags-dir=<value> Import flag values from a directory.
364367
365368
DESCRIPTION
366-
Interact with an active agent, as a user would, to preview responses
369+
Interact with an active agent to preview how the agent responds to your statements, questions, and commands
370+
(utterances).
371+
372+
Use this command to have a natural language conversation with an active agent in your org, as if you were an actual
373+
user. The interface is simple: in the "Start typing..." prompt, enter a statement, question, or command; when you're
374+
done, enter Return. Your utterance is posted on the right along with a timestamp. The agent then responds on the left.
375+
To exit the conversation, hit ESC or Control+C.
376+
377+
This command is useful to test if the agent responds to your utterances as you expect. For example, you can test that
378+
the agent uses a particular topic when asked a question, and then whether it invokes the correct action associated
379+
with that topic. This command is the CLI-equivalent of the Conversation Preview panel in your org's Agent Builder UI.
380+
381+
When the session concludes, the command asks if you want to save the API responses and chat transcripts. By default,
382+
the files are saved to the "./temp/agent-preview" directory. Specify a new default directory by setting the
383+
environment variable "SF_AGENT_PREVIEW_OUTPUT_DIR" to the directory. Or you can pass the directory to the --output-dir
384+
flag.
385+
386+
Find the agent's API name in its main details page in your org's Agent page in Setup.
367387
368-
XXX
388+
Before you use this command, you must complete these steps:
389+
390+
1. Create a connected app in your org as described in the "Create a Connected App" section here:
391+
https://developer.salesforce.com/docs/einstein/genai/guide/agent-api-get-started.html#create-a-connected-app. Do these
392+
additional steps:
393+
394+
1. When specifying the connected app's Callback URL, add this second callback URL on a new line:
395+
http://localhost:1717/OauthRedirect
396+
397+
2. Make note of the user that you specified as the "Run As" user when updating the Client Credentials Flow section.
398+
399+
2. Add the connected app to your agent as described in the "Add Connected App to Agent" section here:
400+
https://developer.salesforce.com/docs/einstein/genai/guide/agent-api-get-started.html#add-connected-app-to-agent.
401+
402+
3. Using the username of the user you specified as the "Run As" user above, authorize your org using the JWT flow, as
403+
described in this document:
404+
https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth_jwt_flow.htm.
405+
406+
4. When you run this command to interact with an agent, specify the username you authorized in the preceding step with
407+
the --connected-app-user (-a) flag.
369408
370409
EXAMPLES
371-
$ sf agent preview --name HelpDeskAgent
410+
Interact with an agent with API name "Resort_Manager" in the org with alias "my-org". Connect to your agent using
411+
the alias "my-jwt-user"; this alias must point to the username who is authorized using JWT:
372412
373-
$ sf agent preview --name ConciergeAgent --target-org production
413+
$ sf agent preview --api-name "Resort_Manager" --target-org my-org --connected-app-user my-jwt-user
374414
375-
FLAG DESCRIPTIONS
376-
-n, --name=<value> The name of the agent you want to preview
415+
Same as the preceding example, but this time save the conversation transcripts to the "./transcripts/my-preview"
416+
directory rather than the default "./temp/agent-preview":
377417
378-
the API name of the agent? (TBD based on agents library)
418+
$ sf agent preview --api-name "Resort_Manager" --target-org my-org --connected-app-user my-jwt-user --output-dir \
419+
"transcripts/my-preview"
379420
```
380421

381422
_See code: [src/commands/agent/preview.ts](https://github.com/salesforcecli/plugin-agent/blob/1.19.5/src/commands/agent/preview.ts)_

command-snapshot.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@
6565
"alias": [],
6666
"command": "agent:preview",
6767
"flagAliases": [],
68-
"flagChars": ["n", "o"],
69-
"flags": ["api-version", "flags-dir", "name", "target-org"],
68+
"flagChars": ["a", "d", "n", "o"],
69+
"flags": ["api-name", "api-version", "connected-app-user", "flags-dir", "output-dir", "target-org"],
7070
"plugin": "@salesforce/plugin-agent"
7171
},
7272
{

messages/agent.preview.md

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,49 @@
11
# summary
22

3-
Interact with an active agent, as a user would, to preview responses
3+
Interact with an active agent to preview how the agent responds to your statements, questions, and commands (utterances).
44

55
# description
66

7-
XXX
7+
Use this command to have a natural language conversation with an active agent in your org, as if you were an actual user. The interface is simple: in the "Start typing..." prompt, enter a statement, question, or command; when you're done, enter Return. Your utterance is posted on the right along with a timestamp. The agent then responds on the left. To exit the conversation, hit ESC or Control+C.
88

9-
# flags.name.summary
9+
This command is useful to test if the agent responds to your utterances as you expect. For example, you can test that the agent uses a particular topic when asked a question, and then whether it invokes the correct action associated with that topic. This command is the CLI-equivalent of the Conversation Preview panel in your org's Agent Builder UI.
1010

11-
The name of the agent you want to preview
11+
When the session concludes, the command asks if you want to save the API responses and chat transcripts. By default, the files are saved to the "./temp/agent-preview" directory. Specify a new default directory by setting the environment variable "SF_AGENT_PREVIEW_OUTPUT_DIR" to the directory. Or you can pass the directory to the --output-dir flag.
1212

13-
# flags.name.description
13+
Find the agent's API name in its main details page in your org's Agent page in Setup.
1414

15-
the API name of the agent? (TBD based on agents library)
15+
Before you use this command, you must complete these steps:
16+
17+
1. Create a connected app in your org as described in the "Create a Connected App" section here: https://developer.salesforce.com/docs/einstein/genai/guide/agent-api-get-started.html#create-a-connected-app. Do these additional steps:
18+
19+
1. When specifying the connected app's Callback URL, add this second callback URL on a new line: http://localhost:1717/OauthRedirect
20+
21+
2. Make note of the user that you specified as the "Run As" user when updating the Client Credentials Flow section.
22+
23+
2. Add the connected app to your agent as described in the "Add Connected App to Agent" section here: https://developer.salesforce.com/docs/einstein/genai/guide/agent-api-get-started.html#add-connected-app-to-agent.
24+
25+
3. Using the username of the user you specified as the "Run As" user above, authorize your org using the JWT flow, as described in this document: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_auth_jwt_flow.htm.
26+
27+
4. When you run this command to interact with an agent, specify the username you authorized in the preceding step with the --connected-app-user (-a) flag.
28+
29+
# flags.api-name.summary
30+
31+
API name of the agent you want to interact with.
32+
33+
# flags.connected-app-user.summary
34+
35+
Username or alias of the connected app user that's configured with JWT-based access tokens to the agent.
36+
37+
# flags.output-dir.summary
38+
39+
Directory where conversation transcripts are saved.
1640

1741
# examples
1842

19-
- <%= config.bin %> <%= command.id %> --name HelpDeskAgent
20-
- <%= config.bin %> <%= command.id %> --name ConciergeAgent --target-org production
43+
- Interact with an agent with API name "Resort_Manager" in the org with alias "my-org". Connect to your agent using the alias "my-jwt-user"; this alias must point to the username who is authorized using JWT:
44+
45+
<%= config.bin %> <%= command.id %> --api-name "Resort_Manager" --target-org my-org --connected-app-user my-jwt-user
46+
47+
- Same as the preceding example, but this time save the conversation transcripts to the "./transcripts/my-preview" directory rather than the default "./temp/agent-preview":
48+
49+
<%= config.bin %> <%= command.id %> --api-name "Resort_Manager" --target-org my-org --connected-app-user my-jwt-user --output-dir "transcripts/my-preview"

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,13 @@
66
"bugs": "https://github.com/forcedotcom/cli/issues",
77
"dependencies": {
88
"@inquirer/core": "^10.1.6",
9-
"@inquirer/figures": "^1.0.7",
109
"@inquirer/prompts": "^7.2.0",
1110
"@oclif/core": "^4",
1211
"@oclif/multi-stage-output": "^0.7.12",
13-
"@salesforce/agents": "0.12.0",
14-
"@salesforce/core": "^8.8.0",
12+
"@salesforce/agents": "0.12.9",
13+
"@salesforce/core": "^8.8.3",
1514
"@salesforce/kit": "^3.2.1",
16-
"@salesforce/sf-plugins-core": "^12.1.0",
15+
"@salesforce/sf-plugins-core": "^12.2.0",
1716
"@salesforce/source-deploy-retrieve": "^12.14.0",
1817
"@salesforce/types": "^1.3.0",
1918
"ansis": "^3.3.2",
@@ -28,8 +27,9 @@
2827
"@oclif/plugin-command-snapshot": "^5.2.19",
2928
"@oclif/test": "^4.1.0",
3029
"@salesforce/cli-plugins-testkit": "^5.3.35",
31-
"@salesforce/dev-scripts": "^10.2.10",
30+
"@salesforce/dev-scripts": "^10.2.12",
3231
"@salesforce/plugin-command-reference": "^3.1.29",
32+
"@types/inquirer": "^9.0.7",
3333
"@types/react": "^18.3.3",
3434
"eslint-config-xo": "^0.45.0",
3535
"eslint-config-xo-react": "^0.27.0",
@@ -217,7 +217,7 @@
217217
"output": []
218218
},
219219
"link-check": {
220-
"command": "node -e \"process.exit(process.env.CI ? 0 : 1)\" || linkinator \"**/*.md\" --skip \"CHANGELOG.md|node_modules|test/|confluence.internal.salesforce.com|my.salesforce.com|%s\" --markdown --retry --directory-listing --verbosity error",
220+
"command": "node -e \"process.exit(process.env.CI ? 0 : 1)\" || linkinator \"**/*.md\" --skip \"CHANGELOG.md|node_modules|test/|confluence.internal.salesforce.com|my.salesforce.com|localhost|%s\" --markdown --retry --directory-listing --verbosity error",
221221
"files": [
222222
"./*.md",
223223
"./!(CHANGELOG).md",

src/commands/agent/preview.ts

Lines changed: 137 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,40 +5,169 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

8+
import { resolve } from 'node:path';
89
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
9-
import { Messages } from '@salesforce/core';
10+
import { Messages, SfError } from '@salesforce/core';
1011
import React from 'react';
1112
import { render } from 'ink';
13+
import { env } from '@salesforce/kit';
14+
import { AgentPreview as Preview } from '@salesforce/agents';
15+
import { select, confirm, input } from '@inquirer/prompts';
1216
import { AgentPreviewReact } from '../../components/agent-preview-react.js';
1317

1418
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
1519
const messages = Messages.loadMessages('@salesforce/plugin-agent', 'agent.preview');
1620

17-
export type AgentPreviewResult = void;
21+
type BotVersionStatus = { Status: 'Active' | 'Inactive' };
22+
23+
export type AgentData = {
24+
Id: string;
25+
DeveloperName: string;
26+
BotVersions: {
27+
records: BotVersionStatus[];
28+
};
29+
};
30+
31+
type Choice<Value> = {
32+
value: Value;
33+
name?: string;
34+
disabled?: boolean | string;
35+
};
1836

37+
type AgentValue = {
38+
Id: string;
39+
DeveloperName: string;
40+
};
41+
42+
// https://developer.salesforce.com/docs/einstein/genai/guide/agent-api-get-started.html#prerequisites
43+
export const UNSUPPORTED_AGENTS = ['Copilot_for_Salesforce'];
44+
45+
export type AgentPreviewResult = void;
1946
export default class AgentPreview extends SfCommand<AgentPreviewResult> {
2047
public static readonly summary = messages.getMessage('summary');
2148
public static readonly description = messages.getMessage('description');
2249
public static readonly examples = messages.getMessages('examples');
2350
public static readonly enableJsonFlag = false;
2451
public static readonly requiresProject = true;
52+
public static state = 'preview';
2553

2654
public static readonly flags = {
2755
'target-org': Flags.requiredOrg(),
2856
'api-version': Flags.orgApiVersion(),
29-
name: Flags.string({
30-
summary: messages.getMessage('flags.name.summary'),
31-
description: messages.getMessage('flags.name.description'),
32-
char: 'n',
57+
'connected-app-user': Flags.requiredOrg({
58+
summary: messages.getMessage('flags.connected-app-user.summary'),
59+
char: 'a',
3360
required: true,
3461
}),
62+
'api-name': Flags.string({
63+
summary: messages.getMessage('flags.api-name.summary'),
64+
char: 'n',
65+
}),
66+
'output-dir': Flags.directory({
67+
summary: messages.getMessage('flags.output-dir.summary'),
68+
char: 'd',
69+
}),
3570
};
3671

3772
public async run(): Promise<AgentPreviewResult> {
3873
const { flags } = await this.parse(AgentPreview);
39-
this.log(`previewing ${flags.name}`);
4074

41-
const instance = render(React.createElement(AgentPreviewReact, null));
75+
const { 'api-name': apiNameFlag } = flags;
76+
const conn = flags['target-org'].getConnection(flags['api-version']);
77+
const apiConn = flags['connected-app-user'].getConnection(flags['api-version']);
78+
79+
const agentsQuery = await conn.query<AgentData>(
80+
'SELECT Id, DeveloperName, (SELECT Status FROM BotVersions) FROM BotDefinition WHERE IsDeleted = false'
81+
);
82+
83+
if (agentsQuery.totalSize === 0) throw new SfError('No Agents found in the org');
84+
85+
const agentsInOrg = agentsQuery.records;
86+
87+
let selectedAgent;
88+
89+
if (apiNameFlag) {
90+
selectedAgent = agentsInOrg.find((agent) => agent.DeveloperName === apiNameFlag);
91+
if (!selectedAgent) throw new Error(`No valid Agents were found with the Api Name ${apiNameFlag}.`);
92+
validateAgent(selectedAgent);
93+
} else {
94+
selectedAgent = await select({
95+
message: 'Select an agent',
96+
choices: getAgentChoices(agentsInOrg),
97+
});
98+
}
99+
100+
const outputDir = await resolveOutputDir(flags['output-dir']);
101+
const agentPreview = new Preview(apiConn);
102+
103+
const instance = render(
104+
React.createElement(AgentPreviewReact, {
105+
agent: agentPreview,
106+
id: selectedAgent.Id,
107+
name: selectedAgent.DeveloperName,
108+
outputDir,
109+
}),
110+
{ exitOnCtrlC: false }
111+
);
42112
await instance.waitUntilExit();
43113
}
44114
}
115+
116+
export const agentIsUnsupported = (devName: string): boolean => UNSUPPORTED_AGENTS.includes(devName);
117+
118+
export const agentIsInactive = (agent: AgentData): boolean =>
119+
// Agent versioning is not fully supported yet, but this should ensure at least one version is active
120+
agent.BotVersions.records.every((botVersion) => botVersion.Status === 'Inactive');
121+
122+
export const validateAgent = (agent: AgentData): boolean => {
123+
// Agents must be active in Agent Builder
124+
if (agentIsInactive(agent)) {
125+
throw new SfError(`Agent ${agent.DeveloperName} is inactive.`);
126+
}
127+
// The default agent is not supported
128+
if (agentIsUnsupported(agent.DeveloperName)) {
129+
throw new SfError(`Agent ${agent.DeveloperName} is not supported.`, 'DefaultAgentNotSupported', [
130+
'See https://developer.salesforce.com/docs/einstein/genai/guide/agent-api-get-started.html#prerequisites',
131+
]);
132+
}
133+
134+
return true;
135+
};
136+
137+
export const getAgentChoices = (agents: AgentData[]): Array<Choice<AgentValue>> =>
138+
agents.map((agent) => {
139+
let disabled: string | boolean = false;
140+
141+
if (agentIsInactive(agent)) disabled = '(Inactive)';
142+
if (agentIsUnsupported(agent.DeveloperName)) disabled = '(Not Supported)';
143+
144+
return {
145+
name: agent.DeveloperName,
146+
value: {
147+
Id: agent.Id,
148+
DeveloperName: agent.DeveloperName,
149+
},
150+
disabled,
151+
};
152+
});
153+
154+
export const resolveOutputDir = async (outputDir: string | undefined): Promise<string | undefined> => {
155+
if (!outputDir) {
156+
const response = await confirm({
157+
message: 'Save transcripts to an output directory?',
158+
default: true,
159+
});
160+
161+
if (response) {
162+
const getDir = await input({
163+
message: 'Enter the output directory',
164+
default: env.getString('SF_AGENT_PREVIEW_OUTPUT_DIR', 'temp/agent-preview'),
165+
required: true,
166+
});
167+
168+
return resolve(getDir);
169+
}
170+
} else {
171+
return resolve(outputDir);
172+
}
173+
};

0 commit comments

Comments
 (0)