From 37007ba6a1004ee72761307b83f7af5b18ebc527 Mon Sep 17 00:00:00 2001 From: Steve Hetzel Date: Fri, 31 Jan 2025 15:41:47 -0700 Subject: [PATCH 1/5] feat: rename to generate agent-spec and more --- command-snapshot.json | 4 +- messages/agent.create.md | 4 ++ ...e.spec.md => agent.generate.agent-spec.md} | 0 ...c.json => agent-generate-agent__spec.json} | 0 src/commands/agent/create.ts | 69 +++++++++++++++---- .../agent/generate/{spec.ts => agent-spec.ts} | 25 +++++-- .../{spec.nut.ts => agent-spec.nut.ts} | 2 +- 7 files changed, 82 insertions(+), 22 deletions(-) rename messages/{agent.generate.spec.md => agent.generate.agent-spec.md} (100%) rename schemas/{agent-generate-spec.json => agent-generate-agent__spec.json} (100%) rename src/commands/agent/generate/{spec.ts => agent-spec.ts} (94%) rename test/commands/agent/generate/{spec.nut.ts => agent-spec.nut.ts} (98%) diff --git a/command-snapshot.json b/command-snapshot.json index 12a663e..abbf6f3 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -19,9 +19,9 @@ }, { "alias": [], - "command": "agent:generate:spec", + "command": "agent:generate:agent-spec", "flagAliases": [], - "flagChars": ["o", "t"], + "flagChars": ["o", "p"], "flags": [ "agent-user", "api-version", diff --git a/messages/agent.create.md b/messages/agent.create.md index 9507c01..707ddbf 100644 --- a/messages/agent.create.md +++ b/messages/agent.create.md @@ -28,6 +28,10 @@ Name (label) of the new agent. API name of the new agent; if not specified, the API name is derived from the agent name (label); the API name must not exist in the org. +# flags.agent-api-name.prompt + +API name of the new agent (default = %s) + # flags.planner-id.summary An existing GenAiPlanner ID to associate with the agent. diff --git a/messages/agent.generate.spec.md b/messages/agent.generate.agent-spec.md similarity index 100% rename from messages/agent.generate.spec.md rename to messages/agent.generate.agent-spec.md diff --git a/schemas/agent-generate-spec.json b/schemas/agent-generate-agent__spec.json similarity index 100% rename from schemas/agent-generate-spec.json rename to schemas/agent-generate-agent__spec.json diff --git a/src/commands/agent/create.ts b/src/commands/agent/create.ts index c3a0831..bcf0558 100644 --- a/src/commands/agent/create.ts +++ b/src/commands/agent/create.ts @@ -5,11 +5,12 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import { resolve } from 'node:path'; -import { readFileSync, writeFileSync } from 'node:fs'; +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import YAML from 'yaml'; import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; import { Lifecycle, Messages } from '@salesforce/core'; import { MultiStageOutput } from '@oclif/multi-stage-output'; +import { input as inquirerInput } from '@inquirer/prompts'; import { colorize } from '@oclif/core/ux'; import { Agent, @@ -20,7 +21,8 @@ import { generateAgentApiName, } from '@salesforce/agents'; import { FlaggablePrompt, makeFlags, promptForFlag, validateAgentType } from '../../flags.js'; -import { AgentSpecFileContents } from './generate/spec.js'; +import { theme } from '../../inquirer-theme.js'; +import { AgentSpecFileContents } from './generate/agent-spec.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-agent', 'agent.create'); @@ -43,6 +45,33 @@ const FLAGGABLE_PROMPTS = { validate: (d: string): boolean | string => d.length > 0 || 'Agent Name cannot be empty', required: true, }, + 'agent-api-name': { + message: messages.getMessage('flags.agent-api-name.summary'), + validate: (d: string): boolean | string => { + if (d.length === 0) { + return true; + } + if (d.length > 80) { + return 'API name cannot be over 80 characters.'; + } + const regex = /^[A-Za-z][A-Za-z0-9_]*[A-Za-z0-9]+$/; + if (!regex.test(d)) { + return 'Invalid API name.'; + } + return true; + }, + }, + spec: { + message: messages.getMessage('flags.spec.summary'), + validate: (d: string): boolean | string => { + const specPath = resolve(d); + if (!existsSync(specPath)) { + return 'Please enter an existing agent spec (yaml) file'; + } + return true; + }, + required: true, + }, } satisfies Record; export default class AgentCreate extends SfCommand { @@ -56,18 +85,9 @@ export default class AgentCreate extends SfCommand { 'target-org': Flags.requiredOrg(), 'api-version': Flags.orgApiVersion(), ...makeFlags(FLAGGABLE_PROMPTS), - spec: Flags.file({ - // char: 'f', - summary: messages.getMessage('flags.spec.summary'), - exists: true, - required: true, - }), preview: Flags.boolean({ summary: messages.getMessage('flags.preview.summary'), }), - 'agent-api-name': Flags.string({ - summary: messages.getMessage('flags.agent-api-name.summary'), - }), // This would be used as more of an agent update than create. // Could possibly move to an `agent update` command. 'planner-id': Flags.string({ @@ -81,17 +101,36 @@ export default class AgentCreate extends SfCommand { const { flags } = await this.parse(AgentCreate); // throw error if --json is used and not all required flags are provided - if (this.jsonEnabled() && !flags['agent-name']) { - throw messages.createError('error.missingRequiredFlags', ['agent-name']); + if (this.jsonEnabled()) { + if (!flags['agent-name']) { + throw messages.createError('error.missingRequiredFlags', ['agent-name']); + } + if (!flags.spec) { + throw messages.createError('error.missingRequiredFlags', ['spec']); + } } + // If we don't have an agent spec yet, prompt. + const specPath = flags.spec ?? (await promptForFlag(FLAGGABLE_PROMPTS['spec'])); + // Read the agent spec and validate - const inputSpec = YAML.parse(readFileSync(resolve(flags.spec), 'utf8')) as AgentSpecFileContents; + const inputSpec = YAML.parse(readFileSync(resolve(specPath), 'utf8')) as AgentSpecFileContents; validateSpec(inputSpec); // If we don't have an agent name yet, prompt. const agentName = flags['agent-name'] ?? (await promptForFlag(FLAGGABLE_PROMPTS['agent-name'])); - const agentApiName = flags['agent-api-name'] ?? generateAgentApiName(agentName); + let agentApiName = flags['agent-api-name']; + if (!agentApiName) { + agentApiName = generateAgentApiName(agentName); + const promptedValue = await inquirerInput({ + message: messages.getMessage('flags.agent-api-name.prompt', [agentApiName]), + validate: FLAGGABLE_PROMPTS['agent-api-name'].validate, + theme, + }); + if (promptedValue?.length) { + agentApiName = promptedValue; + } + } let title: string; const stages = [MSO_STAGES.parse]; diff --git a/src/commands/agent/generate/spec.ts b/src/commands/agent/generate/agent-spec.ts similarity index 94% rename from src/commands/agent/generate/spec.ts rename to src/commands/agent/generate/agent-spec.ts index 05feeba..ed8fd8e 100644 --- a/src/commands/agent/generate/spec.ts +++ b/src/commands/agent/generate/agent-spec.ts @@ -13,7 +13,7 @@ import { Agent, AgentJobSpecCreateConfigV2, AgentJobSpecV2 } from '@salesforce/a import { FlaggablePrompt, makeFlags, promptForFlag, validateAgentType, validateMaxTopics } from '../../../flags.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); -const messages = Messages.loadMessages('@salesforce/plugin-agent', 'agent.generate.spec'); +const messages = Messages.loadMessages('@salesforce/plugin-agent', 'agent.generate.agent-spec'); // The JSON response returned by the command. export type AgentCreateSpecResult = { @@ -34,7 +34,6 @@ export const FLAGGABLE_PROMPTS = { type: { message: messages.getMessage('flags.type.summary'), validate: (d: string): boolean | string => d.length > 0 || 'Type cannot be empty', - char: 't', options: ['customer', 'internal'], required: true, }, @@ -122,7 +121,7 @@ export default class AgentCreateSpec extends SfCommand { }), 'output-file': Flags.file({ summary: messages.getMessage('flags.output-file.summary'), - default: join('config', 'agentSpec.yaml'), + default: join('specs', 'agentSpec.yaml'), }), 'full-interview': Flags.boolean({ summary: messages.getMessage('flags.full-interview.summary'), @@ -136,6 +135,7 @@ export default class AgentCreateSpec extends SfCommand { }), 'no-prompt': Flags.boolean({ summary: messages.getMessage('flags.no-prompt.summary'), + char: 'p', }), }; @@ -275,8 +275,25 @@ const buildSpecFile = ( propertyOrder.map((prop) => { // @ts-expect-error need better typing of the array. // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const val = specResponse[prop] ?? extraProps[prop]; + let val = specResponse[prop] ?? extraProps[prop]; if (val != null || (typeof val === 'string' && val.length > 0)) { + if (prop === 'topics') { + // Ensure topics are [{name, description}] + val = (val as string[]).map((t) => + Object.keys(t) + .sort() + .reverse() + .reduce( + (acc, key) => ({ + // @ts-expect-error need better typing of the array. + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + ...acc, + [key]: t[key], + }), + {} + ) + ); + } // @ts-expect-error need better typing of the array. // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment specFileContents[prop] = val; diff --git a/test/commands/agent/generate/spec.nut.ts b/test/commands/agent/generate/agent-spec.nut.ts similarity index 98% rename from test/commands/agent/generate/spec.nut.ts rename to test/commands/agent/generate/agent-spec.nut.ts index e56abbf..3a6309e 100644 --- a/test/commands/agent/generate/spec.nut.ts +++ b/test/commands/agent/generate/agent-spec.nut.ts @@ -8,7 +8,7 @@ import { join, resolve } from 'node:path'; import { statSync } from 'node:fs'; import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; import { expect } from 'chai'; -import { AgentCreateSpecResult } from '../../../../src/commands/agent/generate/spec.js'; +import { AgentCreateSpecResult } from '../../../../src/commands/agent/generate/agent-spec.js'; describe('agent generate spec NUTs', () => { let session: TestSession; From f28feaa3312d680ea493892a5e06094ed11eb383 Mon Sep 17 00:00:00 2001 From: Steve Hetzel Date: Fri, 31 Jan 2025 15:43:15 -0700 Subject: [PATCH 2/5] chore: format From dbb4f50e70ddf01801b9729fb60d1e262caf9539 Mon Sep 17 00:00:00 2001 From: Steve Hetzel Date: Fri, 31 Jan 2025 15:44:27 -0700 Subject: [PATCH 3/5] chore: format --- src/commands/agent/generate/agent-spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/agent/generate/agent-spec.ts b/src/commands/agent/generate/agent-spec.ts index ed8fd8e..663e909 100644 --- a/src/commands/agent/generate/agent-spec.ts +++ b/src/commands/agent/generate/agent-spec.ts @@ -285,9 +285,9 @@ const buildSpecFile = ( .reverse() .reduce( (acc, key) => ({ + ...acc, // @ts-expect-error need better typing of the array. // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - ...acc, [key]: t[key], }), {} From 55a1161d28af46e1d47d88022e6d3c0e7bfd4fd1 Mon Sep 17 00:00:00 2001 From: Steve Hetzel Date: Fri, 31 Jan 2025 15:54:26 -0700 Subject: [PATCH 4/5] chore: update test --- test/commands/agent/generate/agent-spec.nut.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/commands/agent/generate/agent-spec.nut.ts b/test/commands/agent/generate/agent-spec.nut.ts index 3a6309e..ffe7294 100644 --- a/test/commands/agent/generate/agent-spec.nut.ts +++ b/test/commands/agent/generate/agent-spec.nut.ts @@ -32,7 +32,7 @@ describe('agent generate spec NUTs', () => { const companyName = 'Test Company Name'; const companyDescription = 'Test Company Description'; const companyWebsite = 'https://test-company-website.org'; - const command = `agent generate spec ${targetOrg} --type ${type} --role "${role}" --company-name "${companyName}" --company-description "${companyDescription}" --company-website ${companyWebsite} --json`; + const command = `agent generate agent-spec ${targetOrg} --type ${type} --role "${role}" --company-name "${companyName}" --company-description "${companyDescription}" --company-website ${companyWebsite} --json`; const output = execCmd(command, { ensureExitCode: 0, env: { ...process.env, SF_MOCK_DIR: mockDir }, From 46e5272ecdd7ba4b2011d25fddbf1c525ee5e7b8 Mon Sep 17 00:00:00 2001 From: Steve Hetzel Date: Fri, 31 Jan 2025 16:02:43 -0700 Subject: [PATCH 5/5] chore: update test --- test/commands/agent/generate/agent-spec.nut.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/commands/agent/generate/agent-spec.nut.ts b/test/commands/agent/generate/agent-spec.nut.ts index ffe7294..c4ae5d1 100644 --- a/test/commands/agent/generate/agent-spec.nut.ts +++ b/test/commands/agent/generate/agent-spec.nut.ts @@ -38,7 +38,7 @@ describe('agent generate spec NUTs', () => { env: { ...process.env, SF_MOCK_DIR: mockDir }, }).jsonOutput; - const expectedFilePath = resolve(session.project.dir, 'config', 'agentSpec.yaml'); + const expectedFilePath = resolve(session.project.dir, 'specs', 'agentSpec.yaml'); expect(output?.result.isSuccess).to.be.true; expect(output?.result.specPath).to.equal(expectedFilePath); expect(output?.result.agentType).to.equal(type);