Skip to content

Commit

Permalink
Merge pull request #72 from salesforcecli/sh/more-refactoring
Browse files Browse the repository at this point in the history
W-17669620 - feat:more refactoring
  • Loading branch information
shetzel authored Jan 31, 2025
2 parents 07de93c + 46e5272 commit 6523d82
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 24 deletions.
4 changes: 2 additions & 2 deletions command-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions messages/agent.create.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
File renamed without changes.
File renamed without changes.
69 changes: 54 additions & 15 deletions src/commands/agent/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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');
Expand All @@ -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<string, FlaggablePrompt>;

export default class AgentCreate extends SfCommand<AgentCreateResult> {
Expand All @@ -56,18 +85,9 @@ export default class AgentCreate extends SfCommand<AgentCreateResult> {
'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({
Expand All @@ -81,17 +101,36 @@ export default class AgentCreate extends SfCommand<AgentCreateResult> {
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];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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,
},
Expand Down Expand Up @@ -122,7 +121,7 @@ export default class AgentCreateSpec extends SfCommand<AgentCreateSpecResult> {
}),
'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'),
Expand All @@ -136,6 +135,7 @@ export default class AgentCreateSpec extends SfCommand<AgentCreateSpecResult> {
}),
'no-prompt': Flags.boolean({
summary: messages.getMessage('flags.no-prompt.summary'),
char: 'p',
}),
};

Expand Down Expand Up @@ -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) => ({
...acc,
// @ts-expect-error need better typing of the array.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
[key]: t[key],
}),
{}
)
);
}
// @ts-expect-error need better typing of the array.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
specFileContents[prop] = val;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -32,13 +32,13 @@ 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<AgentCreateSpecResult>(command, {
ensureExitCode: 0,
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);
Expand Down

0 comments on commit 6523d82

Please sign in to comment.