Skip to content

Commit

Permalink
Merge pull request #75 from salesforcecli/sh/more-refactoring
Browse files Browse the repository at this point in the history
W-17669620 - fix: more refactor spec generation and agent create commands
  • Loading branch information
shetzel authored Feb 4, 2025
2 parents 6dc452e + cd9446e commit 7bd99a0
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 30 deletions.
2 changes: 1 addition & 1 deletion messages/agent.create.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ API name of the new agent; if not specified, the API name is derived from the ag

# flags.agent-api-name.prompt

API name of the new agent (default = %s)
API name of the new agent.

# flags.planner-id.summary

Expand Down
10 changes: 9 additions & 1 deletion messages/shared.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,12 @@ agentType must be either "customer" or "internal". Found: [%s]

# error.invalidMaxTopics

maxNumOfTopics must be a number greater than 0. Found: [%s]
maxNumOfTopics must be a number between 1-30. Found: [%s]

# error.invalidTone

tone must be one of ['formal', 'casual', 'neutral']. Found: [%s]

# error.invalidAgentUser

agentUser must be the username of an existing user in the org. Found: [%s]
17 changes: 12 additions & 5 deletions src/commands/agent/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@ import {
AgentCreateResponseV2,
generateAgentApiName,
} from '@salesforce/agents';
import { FlaggablePrompt, makeFlags, promptForFlag, validateAgentType } from '../../flags.js';
import {
FlaggablePrompt,
makeFlags,
promptForFlag,
getAgentUserId,
validateAgentType,
validateTone,
} from '../../flags.js';
import { theme } from '../../inquirer-theme.js';
import { AgentSpecFileContents } from './generate/agent-spec.js';

Expand Down Expand Up @@ -123,8 +130,9 @@ export default class AgentCreate extends SfCommand<AgentCreateResult> {
if (!agentApiName) {
agentApiName = generateAgentApiName(agentName);
const promptedValue = await inquirerInput({
message: messages.getMessage('flags.agent-api-name.prompt', [agentApiName]),
message: messages.getMessage('flags.agent-api-name.prompt'),
validate: FLAGGABLE_PROMPTS['agent-api-name'].validate,
default: agentApiName,
theme,
});
if (promptedValue?.length) {
Expand Down Expand Up @@ -178,14 +186,13 @@ export default class AgentCreate extends SfCommand<AgentCreateResult> {
agentConfig.agentSettings.plannerId = flags['planner-id'];
}
if (inputSpec?.agentUser) {
// TODO: query for the user ID from the username
agentConfig.agentSettings.userId = inputSpec.agentUser;
agentConfig.agentSettings.userId = await getAgentUserId(connection, inputSpec.agentUser);
}
if (inputSpec?.enrichLogs) {
agentConfig.agentSettings.enrichLogs = inputSpec.enrichLogs;
}
if (inputSpec?.tone) {
agentConfig.agentSettings.tone = inputSpec.tone;
agentConfig.agentSettings.tone = validateTone(inputSpec.tone);
}
}
const response = await agent.createV2(agentConfig);
Expand Down
60 changes: 39 additions & 21 deletions src/commands/agent/generate/agent-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@ import { SfCommand, Flags, prompts } from '@salesforce/sf-plugins-core';
import { Messages } from '@salesforce/core';
import YAML from 'yaml';
import { Agent, AgentJobSpecCreateConfigV2, AgentJobSpecV2 } from '@salesforce/agents';
import { FlaggablePrompt, makeFlags, promptForFlag, validateAgentType, validateMaxTopics } from '../../../flags.js';
import {
FlaggablePrompt,
makeFlags,
promptForFlag,
validateAgentType,
validateMaxTopics,
validateTone,
validateAgentUser,
} from '../../../flags.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-agent', 'agent.generate.agent-spec');
Expand All @@ -22,11 +30,13 @@ export type AgentCreateSpecResult = {
specPath?: string; // the location of the job spec file
} & AgentJobSpecV2;

type AgentTone = 'casual' | 'formal' | 'neutral';

// Agent spec file schema
export type AgentSpecFileContents = AgentJobSpecV2 & {
agentUser?: string;
enrichLogs?: boolean;
tone?: 'casual' | 'formal' | 'neutral';
tone?: AgentTone;
primaryLanguage?: 'en_US';
};

Expand Down Expand Up @@ -62,7 +72,6 @@ export const FLAGGABLE_PROMPTS = {
const regExp = new RegExp('^(http|https)://', 'i');
const companySite = regExp.test(d) ? d : `https://${d}`;
new URL(companySite);
d = companySite;
return true;
} catch (e) {
return 'Please enter a valid URL';
Expand All @@ -72,9 +81,17 @@ export const FLAGGABLE_PROMPTS = {
'max-topics': {
message: messages.getMessage('flags.max-topics.summary'),
promptMessage: messages.getMessage('flags.max-topics.prompt'),
validate: (): boolean | string => true,
// min: 1,
// max: 30,
validate: (d: string): boolean | string => {
if (d.length === 0) return true;

// convert to a number
const maxTopics = Number(d.trim());
if (typeof maxTopics === 'number' && maxTopics > 0 && maxTopics < 31) {
return true;
}
return 'Please enter a number between 1-30';
},
default: '5',
},
'agent-user': {
message: messages.getMessage('flags.agent-user.summary'),
Expand Down Expand Up @@ -143,14 +160,7 @@ export default class AgentCreateSpec extends SfCommand<AgentCreateSpecResult> {
public async run(): Promise<AgentCreateSpecResult> {
const { flags } = await this.parse(AgentCreateSpec);

let outputFile: string;
try {
outputFile = await resolveOutputFile(flags['output-file'], flags['no-prompt']);
} catch (e) {
this.log(messages.getMessage('commandCanceled'));
// @ts-expect-error expected due to command cancelation.
return;
}
const outputFile = await resolveOutputFile(flags['output-file'], flags['no-prompt']);

// throw error if --json is used and not all required flags are provided
if (this.jsonEnabled()) {
Expand Down Expand Up @@ -182,6 +192,8 @@ export default class AgentCreateSpec extends SfCommand<AgentCreateSpecResult> {
(await promptForFlag(FLAGGABLE_PROMPTS['company-description']));
const role = flags.role ?? inputSpec?.role ?? (await promptForFlag(FLAGGABLE_PROMPTS.role));

const connection = flags['target-org'].getConnection(flags['api-version']);

// full interview prompts
const companyWebsite =
flags['company-website'] ??
Expand All @@ -190,20 +202,23 @@ export default class AgentCreateSpec extends SfCommand<AgentCreateSpecResult> {
const maxNumOfTopics =
flags['max-topics'] ??
validateMaxTopics(inputSpec?.maxNumOfTopics) ??
(flags['full-interview'] ? await promptForFlag(FLAGGABLE_PROMPTS['max-topics']) : 10);
(flags['full-interview'] ? await promptForFlag(FLAGGABLE_PROMPTS['max-topics']) : 5) ??
5;
const agentUser =
flags['agent-user'] ??
inputSpec?.agentUser ??
(flags['full-interview'] ? await promptForFlag(FLAGGABLE_PROMPTS['agent-user']) : undefined);
await validateAgentUser(connection, agentUser);
let enrichLogs =
flags['enrich-logs'] ??
inputSpec?.enrichLogs ??
(flags['full-interview'] ? await promptForFlag(FLAGGABLE_PROMPTS['enrich-logs']) : undefined);
enrichLogs = Boolean(enrichLogs === 'true' || enrichLogs === true);
const tone =
let tone =
flags.tone ??
inputSpec?.tone ??
(flags['full-interview'] ? await promptForFlag(FLAGGABLE_PROMPTS.tone) : undefined);
(flags['full-interview'] ? await promptForFlag(FLAGGABLE_PROMPTS.tone) : 'casual');
tone = validateTone(tone as AgentTone);
// const primaryLanguage =
// flags['primary-language'] ??
// inputSpec?.primaryLanguage ??
Expand All @@ -212,7 +227,6 @@ export default class AgentCreateSpec extends SfCommand<AgentCreateSpecResult> {
this.log();
this.spinner.start('Creating agent spec');

const connection = flags['target-org'].getConnection(flags['api-version']);
const agent = new Agent(connection, this.project!);
const specConfig: AgentJobSpecCreateConfigV2 = {
agentType: type as 'customer' | 'internal',
Expand All @@ -234,8 +248,7 @@ export default class AgentCreateSpec extends SfCommand<AgentCreateSpecResult> {
if (maxNumOfTopics) {
specConfig.maxNumOfTopics = Number(maxNumOfTopics);
}
// Should we log the specConfig being used? It's returned in the JSON and the generated spec.
// this.log(`${ansis.green(figures.tick)} ${ansis.bold(message)} ${ansis.cyan(valueFromFlag)}`);

const specResponse = await agent.createSpecV2(specConfig);
// @ts-expect-error Need better typing
const specFileContents = buildSpecFile(specResponse, { agentUser, enrichLogs, tone });
Expand Down Expand Up @@ -319,7 +332,12 @@ const resolveOutputFile = async (outputFile: string, noPrompt = false): Promise<
if (existsSync(resolvedOutputFile)) {
const message = messages.getMessage('confirmSpecOverwrite', [resolvedOutputFile]);
if (!(await prompts.confirm({ message }))) {
throw Error('NoOverwrite');
return resolveOutputFile(
await promptForFlag({
message: messages.getMessage('flags.output-file.summary'),
validate: (d: string): boolean | string => d.length > 0 || 'Output file cannot be empty',
})
);
}
}
}
Expand Down
34 changes: 32 additions & 2 deletions src/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import { Interfaces } from '@oclif/core';
import { Flags } from '@salesforce/sf-plugins-core';
import { Messages } from '@salesforce/core';
import { Connection, Messages, SfError } from '@salesforce/core';
import { camelCaseToTitleCase } from '@salesforce/kit';
import { select, input as inquirerInput } from '@inquirer/prompts';
import { theme } from './inquirer-theme.js';
Expand All @@ -30,6 +30,8 @@ type FlagsOfPrompts<T extends Record<string, FlaggablePrompt>> = Record<
Interfaces.OptionFlag<string | undefined, Interfaces.CustomOptions>
>;

type AgentTone = 'casual' | 'formal' | 'neutral';

export const resultFormatFlag = Flags.option({
options: ['json', 'human', 'junit', 'tap'] as const,
default: 'human',
Expand Down Expand Up @@ -99,10 +101,38 @@ export const validateMaxTopics = (maxTopics?: number): number | undefined => {
// Deliberately using: != null
if (maxTopics != null) {
if (!isNaN(maxTopics) && isFinite(maxTopics)) {
if (maxTopics > 0) {
if (maxTopics > 0 && maxTopics < 31) {
return maxTopics;
}
}
throw messages.createError('error.invalidMaxTopics', [maxTopics]);
}
};

export const validateTone = (tone: AgentTone): AgentTone => {
if (!['formal', 'casual', 'neutral'].includes(tone)) {
throw messages.createError('error.invalidTone', [tone]);
}
return tone;
};

export const validateAgentUser = async (connection: Connection, agentUser?: string): Promise<void> => {
if (agentUser?.length) {
try {
const q = `SELECT Id FROM User WHERE Username = '${agentUser}'`;
await connection.singleRecordQuery<{ Id: string }>(q);
} catch (error) {
const err = SfError.wrap(error);
throw SfError.create({
name: 'InvalidAgentUser',
message: messages.getMessage('error.invalidAgentUser', [agentUser]),
cause: err,
});
}
}
};

export const getAgentUserId = async (connection: Connection, agentUser: string): Promise<string> => {
const q = `SELECT Id FROM User WHERE Username = '${agentUser}'`;
return (await connection.singleRecordQuery<{ Id: string }>(q)).Id;
};

0 comments on commit 7bd99a0

Please sign in to comment.