From 633e4849c304bc54ce3b028497607efd5007299b Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Mon, 11 Nov 2024 13:04:42 -0700 Subject: [PATCH 1/4] feat: add multi-stage-output --- command-snapshot.json | 8 +-- messages/agent.create.md | 8 ++- ....create.spec.md => agent.generate.spec.md} | 8 +-- package.json | 1 + src/commands/agent/create.ts | 52 +++++++++++++++++-- src/commands/agent/generate/spec.ts | 33 +++++++----- yarn.lock | 27 +++++++++- 7 files changed, 111 insertions(+), 26 deletions(-) rename messages/{agent.create.spec.md => agent.generate.spec.md} (91%) diff --git a/command-snapshot.json b/command-snapshot.json index 1f4d532..e017eed 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -5,13 +5,15 @@ "flagAliases": [], "flagChars": [ "f", + "n", "o" ], "flags": [ "api-version", "flags-dir", + "job-spec", "json", - "spec", + "name", "target-org" ], "plugin": "@salesforce/plugin-agent" @@ -22,7 +24,7 @@ "flagAliases": [], "flagChars": [ "d", - "n", + "f", "o", "t" ], @@ -31,9 +33,9 @@ "company-description", "company-name", "company-website", + "file-name", "flags-dir", "json", - "name", "output-dir", "role", "target-org", diff --git a/messages/agent.create.md b/messages/agent.create.md index 6597802..c3eb956 100644 --- a/messages/agent.create.md +++ b/messages/agent.create.md @@ -6,14 +6,18 @@ Create an Agent from an agent spec. Create an Agent from an agent spec. Agent metadata is created in the target org and retrieved to the local project. -# flags.spec.summary +# flags.job-spec.summary The path to an agent spec file. -# flags.spec.description +# flags.job-spec.description The agent spec file defines job titles and descriptions for the agent and can be created using the `sf agent create spec` command. +# flags.name.summary + +The name of the agent. + # examples - Create an Agent: diff --git a/messages/agent.create.spec.md b/messages/agent.generate.spec.md similarity index 91% rename from messages/agent.create.spec.md rename to messages/agent.generate.spec.md index 17b5eb6..91e4867 100644 --- a/messages/agent.create.spec.md +++ b/messages/agent.generate.spec.md @@ -6,10 +6,6 @@ Create an Agent spec. Create an Agent spec, which is a list of job titles and descriptions that the agent performs. -# flags.name.summary - -The name of the agent to create. - # flags.type.summary The type of agent to create. @@ -34,6 +30,10 @@ The website URL for the company. The location within the project where the agent spec will be written. +# flags.file-name.summary + +The name of the file to write the agent spec to. + # examples - Create an Agent spec in the default location: diff --git a/package.json b/package.json index 82d37d1..62e084a 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "@inquirer/input": "^4.0.1", "@inquirer/select": "^4.0.1", "@oclif/core": "^4", + "@oclif/multi-stage-output": "^0.7.12", "@salesforce/core": "^8.5.2", "@salesforce/kit": "^3.2.1", "@salesforce/sf-plugins-core": "^12", diff --git a/src/commands/agent/create.ts b/src/commands/agent/create.ts index b90d51f..657ff95 100644 --- a/src/commands/agent/create.ts +++ b/src/commands/agent/create.ts @@ -8,6 +8,8 @@ import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; import { Messages } from '@salesforce/core'; import { Duration, sleep } from '@salesforce/kit'; +import { MultiStageOutput } from '@oclif/multi-stage-output'; +import { colorize } from '@oclif/core/ux'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-agent', 'agent.create'); @@ -29,24 +31,66 @@ export default class AgentCreate extends SfCommand { public static readonly flags = { 'target-org': Flags.requiredOrg(), 'api-version': Flags.orgApiVersion(), - spec: Flags.file({ + 'job-spec': Flags.file({ char: 'f', required: true, - summary: messages.getMessage('flags.spec.summary'), - description: messages.getMessage('flags.spec.description'), + summary: messages.getMessage('flags.job-spec.summary'), + description: messages.getMessage('flags.job-spec.description'), + }), + name: Flags.string({ + char: 'n', + required: true, + summary: messages.getMessage('flags.name.summary'), }), }; public async run(): Promise { const { flags } = await this.parse(AgentCreate); + const mso = new MultiStageOutput<{ file?: string }>({ + jsonEnabled: this.jsonEnabled(), + title: `Creating ${flags.name} Agent`, + stages: [ + 'Parsing agent spec JSON', + 'Generating GenAiPlanner metadata', + 'Creating agent in org', + 'Retrieving agent metadata', + ], + stageSpecificBlock: [ + { + stage: 'Parsing agent spec JSON', + get: (data) => data?.file, + type: 'static-key-value', + label: 'file', + }, + ], + }); + + mso.goto('Parsing agent spec JSON', { file: flags['job-spec'] }); + await sleep(Duration.milliseconds(200)); + + mso.goto('Generating GenAiPlanner metadata'); + await sleep(Duration.milliseconds(200)); - this.log(`Creating agent from spec: ${flags.spec}`); + mso.goto('Creating agent in org'); // POST to /services/data/{api-version}/connect/attach-agent-topics // To simulate time spent on the server generating the spec. await sleep(Duration.seconds(5)); + mso.goto('Retrieving agent metadata'); + await sleep(Duration.seconds(3)); + + mso.stop(); + + this.log( + colorize( + 'green', + `Successfully created ${flags.name} in ${flags['target-org'].getUsername() ?? 'the target org'}.` + ) + ); + this.log(`Use ${colorize('dim', `sf agent open --agent ${flags.name}`)} to view the agent in the browser.`); + return { isSuccess: true }; } } diff --git a/src/commands/agent/generate/spec.ts b/src/commands/agent/generate/spec.ts index b7bc97d..d9f3c12 100644 --- a/src/commands/agent/generate/spec.ts +++ b/src/commands/agent/generate/spec.ts @@ -13,10 +13,11 @@ import ansis from 'ansis'; import select from '@inquirer/select'; import inquirerInput from '@inquirer/input'; import figures from '@inquirer/figures'; +// eslint-disable-next-line import/no-extraneous-dependencies import { Agent, SfAgent } from '@salesforce/agents'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); -const messages = Messages.loadMessages('@salesforce/plugin-agent', 'agent.create.spec'); +const messages = Messages.loadMessages('@salesforce/plugin-agent', 'agent.generate.spec'); export type AgentCreateSpecResult = { isSuccess: boolean; @@ -40,12 +41,6 @@ type FlagsOfPrompts> = Record< >; const FLAGGABLE_PROMPTS = { - name: { - message: messages.getMessage('flags.name.summary'), - validate: (d: string): boolean | string => d.length > 0 || 'Name cannot be empty', - char: 'n', - required: true, - }, type: { message: messages.getMessage('flags.type.summary'), validate: (d: string): boolean | string => d.length > 0 || 'Type cannot be empty', @@ -124,6 +119,11 @@ export default class AgentCreateSpec extends SfCommand { summary: messages.getMessage('flags.output-dir.summary'), default: 'config', }), + 'file-name': Flags.string({ + char: 'f', + summary: messages.getMessage('flags.file-name.summary'), + default: 'agentSpec.json', + }), }; public async run(): Promise { @@ -142,11 +142,15 @@ export default class AgentCreateSpec extends SfCommand { this.log(); this.styledHeader('Agent Details'); - const name = await this.getFlagOrPrompt(flags.name, FLAGGABLE_PROMPTS.name); - const type = await this.getFlagOrPrompt(flags.type, FLAGGABLE_PROMPTS.type) as 'customer_facing' | 'employee_facing'; + const type = (await this.getFlagOrPrompt(flags.type, FLAGGABLE_PROMPTS.type)) as + | 'customer_facing' + | 'employee_facing'; const role = await this.getFlagOrPrompt(flags.role, FLAGGABLE_PROMPTS.role); const companyName = await this.getFlagOrPrompt(flags['company-name'], FLAGGABLE_PROMPTS['company-name']); - const companyDescription = await this.getFlagOrPrompt(flags['company-description'], FLAGGABLE_PROMPTS['company-description']); + const companyDescription = await this.getFlagOrPrompt( + flags['company-description'], + FLAGGABLE_PROMPTS['company-description'] + ); const companyWebsite = await this.getFlagOrPrompt(flags['company-website'], FLAGGABLE_PROMPTS['company-website']); this.log(); @@ -155,11 +159,16 @@ export default class AgentCreateSpec extends SfCommand { const connection = flags['target-org'].getConnection(flags['api-version']); const agent = new Agent(connection, this.project as SfProject) as SfAgent; const agentSpec = await agent.createSpec({ - name, type, role, companyName, companyDescription, companyWebsite + name: flags['file-name'].split('.json')[0], + type, + role, + companyName, + companyDescription, + companyWebsite, }); // Write a file with the returned job specs - const filePath = join(flags['output-dir'], 'agentSpec.json'); + const filePath = join(flags['output-dir'], flags['file-name']); writeFileSync(filePath, JSON.stringify(agentSpec, null, 4)); this.spinner.stop(); diff --git a/yarn.lock b/yarn.lock index 3e5919b..32fd5d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1293,6 +1293,19 @@ wordwrap "^1.0.0" wrap-ansi "^7.0.0" +"@oclif/multi-stage-output@^0.7.12": + version "0.7.12" + resolved "https://registry.yarnpkg.com/@oclif/multi-stage-output/-/multi-stage-output-0.7.12.tgz#04df5efb6dce527920cf475c9ad9f20236803ccd" + integrity sha512-MmCgqPb7jC7QUOiX9jMik2njplCO+tRybGxiB55OXDmZ7osifM3KuQb/ykgP4XYn559k3DXeNLFS0NpokuH+mw== + dependencies: + "@oclif/core" "^4" + "@types/react" "^18.3.12" + cli-spinners "^2" + figures "^6.1.0" + ink "^5.0.1" + react "^18.3.1" + wrap-ansi "^9.0.0" + "@oclif/plugin-command-snapshot@^5.2.19": version "5.2.19" resolved "https://registry.yarnpkg.com/@oclif/plugin-command-snapshot/-/plugin-command-snapshot-5.2.19.tgz#02f3f2c426aa0791bfcc598c9210e061f98caf54" @@ -2958,7 +2971,7 @@ cli-progress@^3.12.0: dependencies: string-width "^4.2.3" -cli-spinners@^2.9.2: +cli-spinners@^2, cli-spinners@^2.9.2: version "2.9.2" resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41" integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== @@ -3881,6 +3894,13 @@ faye@^1.4.0: tough-cookie "*" tunnel-agent "*" +figures@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-6.1.0.tgz#935479f51865fa7479f6fa94fc6fc7ac14e62c4a" + integrity sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg== + dependencies: + is-unicode-supported "^2.0.0" + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -4780,6 +4800,11 @@ is-unicode-supported@^0.1.0: resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== +is-unicode-supported@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz#09f0ab0de6d3744d48d265ebb98f65d11f2a9b3a" + integrity sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ== + is-weakref@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" From 10fcd5ed44ecc41316fdb17cc54cf44da6815e2b Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Mon, 11 Nov 2024 13:55:04 -0700 Subject: [PATCH 2/4] fix: update UX --- src/commands/agent/create.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/commands/agent/create.ts b/src/commands/agent/create.ts index 657ff95..01b7622 100644 --- a/src/commands/agent/create.ts +++ b/src/commands/agent/create.ts @@ -46,26 +46,19 @@ export default class AgentCreate extends SfCommand { public async run(): Promise { const { flags } = await this.parse(AgentCreate); - const mso = new MultiStageOutput<{ file?: string }>({ + const jsonParsingStage = `Parsing ${flags['job-spec']}`; + const mso = new MultiStageOutput({ jsonEnabled: this.jsonEnabled(), title: `Creating ${flags.name} Agent`, stages: [ - 'Parsing agent spec JSON', + jsonParsingStage, 'Generating GenAiPlanner metadata', 'Creating agent in org', 'Retrieving agent metadata', ], - stageSpecificBlock: [ - { - stage: 'Parsing agent spec JSON', - get: (data) => data?.file, - type: 'static-key-value', - label: 'file', - }, - ], }); - mso.goto('Parsing agent spec JSON', { file: flags['job-spec'] }); + mso.goto(jsonParsingStage); await sleep(Duration.milliseconds(200)); mso.goto('Generating GenAiPlanner metadata'); From f37d979937eb7bbc19373bcca470d569facc4a2b Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Tue, 12 Nov 2024 10:22:02 -0700 Subject: [PATCH 3/4] fix: update success message --- src/commands/agent/create.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/agent/create.ts b/src/commands/agent/create.ts index 01b7622..7d0cd6e 100644 --- a/src/commands/agent/create.ts +++ b/src/commands/agent/create.ts @@ -82,7 +82,7 @@ export default class AgentCreate extends SfCommand { `Successfully created ${flags.name} in ${flags['target-org'].getUsername() ?? 'the target org'}.` ) ); - this.log(`Use ${colorize('dim', `sf agent open --agent ${flags.name}`)} to view the agent in the browser.`); + this.log(`Use ${colorize('dim', `sf org open agent --name ${flags.name}`)} to view the agent in the browser.`); return { isSuccess: true }; } From 7dede9045f8cafcb17c54642cfbde3b9e89e347e Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Tue, 12 Nov 2024 13:33:49 -0700 Subject: [PATCH 4/4] chore: clean up --- src/commands/agent/generate/spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/commands/agent/generate/spec.ts b/src/commands/agent/generate/spec.ts index d9f3c12..fea0d42 100644 --- a/src/commands/agent/generate/spec.ts +++ b/src/commands/agent/generate/spec.ts @@ -13,7 +13,6 @@ import ansis from 'ansis'; import select from '@inquirer/select'; import inquirerInput from '@inquirer/input'; import figures from '@inquirer/figures'; -// eslint-disable-next-line import/no-extraneous-dependencies import { Agent, SfAgent } from '@salesforce/agents'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);