|
5 | 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause |
6 | 6 | */ |
7 | 7 |
|
| 8 | +import { resolve } from 'node:path'; |
8 | 9 | import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; |
9 | | -import { Messages } from '@salesforce/core'; |
| 10 | +import { Messages, SfError } from '@salesforce/core'; |
10 | 11 | import React from 'react'; |
11 | 12 | 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'; |
12 | 16 | import { AgentPreviewReact } from '../../components/agent-preview-react.js'; |
13 | 17 |
|
14 | 18 | Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); |
15 | 19 | const messages = Messages.loadMessages('@salesforce/plugin-agent', 'agent.preview'); |
16 | 20 |
|
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 | +}; |
18 | 36 |
|
| 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; |
19 | 46 | export default class AgentPreview extends SfCommand<AgentPreviewResult> { |
20 | 47 | public static readonly summary = messages.getMessage('summary'); |
21 | 48 | public static readonly description = messages.getMessage('description'); |
22 | 49 | public static readonly examples = messages.getMessages('examples'); |
23 | 50 | public static readonly enableJsonFlag = false; |
24 | 51 | public static readonly requiresProject = true; |
| 52 | + public static state = 'preview'; |
25 | 53 |
|
26 | 54 | public static readonly flags = { |
27 | 55 | 'target-org': Flags.requiredOrg(), |
28 | 56 | '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', |
33 | 60 | required: true, |
34 | 61 | }), |
| 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 | + }), |
35 | 70 | }; |
36 | 71 |
|
37 | 72 | public async run(): Promise<AgentPreviewResult> { |
38 | 73 | const { flags } = await this.parse(AgentPreview); |
39 | | - this.log(`previewing ${flags.name}`); |
40 | 74 |
|
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 | + ); |
42 | 112 | await instance.waitUntilExit(); |
43 | 113 | } |
44 | 114 | } |
| 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