Skip to content

Commit 4805a2f

Browse files
committed
feat: add ability to create a spec from a spec
1 parent 3d5261b commit 4805a2f

File tree

3 files changed

+108
-70
lines changed

3 files changed

+108
-70
lines changed

command-snapshot.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,20 +39,20 @@
3939
"alias": [],
4040
"command": "agent:generate:spec-v2",
4141
"flagAliases": [],
42-
"flagChars": ["d", "f", "o", "t"],
42+
"flagChars": ["o", "t"],
4343
"flags": [
4444
"api-version",
4545
"company-description",
4646
"company-name",
4747
"company-website",
48-
"file-name",
4948
"flags-dir",
5049
"grounding-context",
5150
"json",
5251
"max-topics",
53-
"output-dir",
52+
"output-file",
5453
"prompt-template",
5554
"role",
55+
"spec",
5656
"target-org",
5757
"type"
5858
],

messages/agent.generate.spec-v2.md

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,9 @@ Description of your company.
3030

3131
Website URL of your company.
3232

33-
# flags.output-dir.summary
33+
# flags.output-file.summary
3434

35-
Directory where the agent spec file is written; can be an absolute or relative path.
36-
37-
# flags.file-name.summary
38-
39-
Name of the generated agent spec file (yaml).
35+
Path for the generated agent spec file (yaml); can be an absolute or relative path.
4036

4137
# flags.max-topics.summary
4238

@@ -50,6 +46,10 @@ Developer name of a customized prompt template to use instead of the default.
5046

5147
Context information to be used with the customized prompt template.
5248

49+
# flags.spec.summary
50+
51+
Spec file (yaml) to use as input to the command.
52+
5353
# examples
5454

5555
- Create an agent spec for your default org in the default location and use flags to specify the agent's role and your company details:
@@ -59,3 +59,15 @@ Context information to be used with the customized prompt template.
5959
- Create an agent spec by being prompted for role and company details interactively; write the generated file to the "specs" directory and use the org with alias "my-org":
6060

6161
<%= config.bin %> <%= command.id %> --output-dir specs --target-org my-org
62+
63+
# error.invalidAgentType
64+
65+
agentType must be either "customer" or "internal". Found: [%s]
66+
67+
# error.invalidMaxTopics
68+
69+
maxNumOfTopics must be a number greater than 0. Found: [%s]
70+
71+
# error.missingRequiredFlags
72+
73+
Missing required flags: %s

src/commands/agent/generate/spec-v2.ts

Lines changed: 87 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,14 @@
44
* Licensed under the BSD 3-Clause license.
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
7-
import { join } from 'node:path';
8-
import { mkdirSync, writeFileSync } from 'node:fs';
7+
import { join, resolve, dirname } from 'node:path';
8+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
99
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
1010
import { Messages, SfProject } from '@salesforce/core';
1111
import { Interfaces } from '@oclif/core';
12-
import ansis from 'ansis';
1312
import YAML from 'yaml';
1413
import select from '@inquirer/select';
1514
import inquirerInput from '@inquirer/input';
16-
import figures from '@inquirer/figures';
1715
import { Agent, AgentJobSpecCreateConfigV2, AgentJobSpecV2 } from '@salesforce/agents';
1816
import { theme } from '../../../inquirer-theme.js';
1917

@@ -113,15 +111,13 @@ export default class AgentCreateSpecV2 extends SfCommand<AgentCreateSpecResult>
113111
'target-org': Flags.requiredOrg(),
114112
'api-version': Flags.orgApiVersion(),
115113
...makeFlags(FLAGGABLE_PROMPTS),
116-
'output-dir': Flags.directory({
117-
char: 'd',
118-
summary: messages.getMessage('flags.output-dir.summary'),
119-
default: 'config',
114+
spec: Flags.file({
115+
summary: messages.getMessage('flags.spec.summary'),
116+
exists: true,
120117
}),
121-
'file-name': Flags.string({
122-
char: 'f',
123-
summary: messages.getMessage('flags.file-name.summary'),
124-
default: 'agentSpec.yaml',
118+
'output-file': Flags.file({
119+
summary: messages.getMessage('flags.output-file.summary'),
120+
default: join('config', 'agentSpec.yaml'),
125121
}),
126122
'max-topics': Flags.integer({
127123
summary: messages.getMessage('flags.max-topics.summary'),
@@ -136,6 +132,7 @@ export default class AgentCreateSpecV2 extends SfCommand<AgentCreateSpecResult>
136132
}),
137133
};
138134

135+
// eslint-disable-next-line complexity
139136
public async run(): Promise<AgentCreateSpecResult> {
140137
const { flags } = await this.parse(AgentCreateSpecV2);
141138

@@ -146,88 +143,117 @@ export default class AgentCreateSpecV2 extends SfCommand<AgentCreateSpecResult>
146143
.map(([key]) => key);
147144

148145
if (missingFlags.length) {
149-
throw new Error(`Missing required flags: ${missingFlags.join(', ')}`);
146+
throw messages.createError('error.missingRequiredFlags', [missingFlags.join(', ')]);
150147
}
151148
}
152149

153150
this.log();
154151
this.styledHeader('Agent Details');
155-
const type = (await this.getFlagOrPrompt(flags.type, FLAGGABLE_PROMPTS.type)) as 'customer' | 'internal';
156-
const role = await this.getFlagOrPrompt(flags.role, FLAGGABLE_PROMPTS.role);
157-
const companyName = await this.getFlagOrPrompt(flags['company-name'], FLAGGABLE_PROMPTS['company-name']);
158-
const companyDescription = await this.getFlagOrPrompt(
159-
flags['company-description'],
160-
FLAGGABLE_PROMPTS['company-description']
161-
);
162-
const companyWebsite = await this.getFlagOrPrompt(flags['company-website'], FLAGGABLE_PROMPTS['company-website']);
152+
153+
// If spec is provided, read it first
154+
let inputSpec: Partial<AgentJobSpecV2> = {};
155+
if (flags.spec) {
156+
inputSpec = YAML.parse(readFileSync(resolve(flags.spec), 'utf8')) as Partial<AgentJobSpecV2>;
157+
}
158+
159+
// Flags override inputSpec values. Prompt if neither is set.
160+
const type = flags.type ?? validateAgentType(inputSpec?.agentType) ?? (await promptForFlag(FLAGGABLE_PROMPTS.type));
161+
const role = flags.role ?? inputSpec?.role ?? (await promptForFlag(FLAGGABLE_PROMPTS.role));
162+
const companyName =
163+
flags['company-name'] ?? inputSpec?.companyName ?? (await promptForFlag(FLAGGABLE_PROMPTS['company-name']));
164+
const companyDescription =
165+
flags['company-description'] ??
166+
inputSpec?.companyDescription ??
167+
(await promptForFlag(FLAGGABLE_PROMPTS['company-description']));
168+
const companyWebsite =
169+
flags['company-website'] ??
170+
inputSpec?.companyWebsite ??
171+
(await promptForFlag(FLAGGABLE_PROMPTS['company-website']));
163172

164173
this.log();
165174
this.spinner.start('Creating agent spec');
166175

167176
const connection = flags['target-org'].getConnection(flags['api-version']);
168177
const agent = new Agent(connection, this.project as SfProject);
169178
const specConfig: AgentJobSpecCreateConfigV2 = {
170-
agentType: type,
179+
agentType: type as 'customer' | 'internal',
171180
role,
172181
companyName,
173182
companyDescription,
174183
};
175184
if (companyWebsite) {
176185
specConfig.companyWebsite = companyWebsite;
177186
}
178-
if (flags['prompt-template']) {
179-
specConfig.promptTemplateName = flags['prompt-template'];
180-
if (flags['grounding-context']) {
181-
specConfig.groundingContext = flags['grounding-context'];
187+
const promptTemplateName = flags['prompt-template'] ?? inputSpec?.promptTemplateName;
188+
if (promptTemplateName) {
189+
specConfig.promptTemplateName = promptTemplateName;
190+
const groundingContext = flags['grounding-context'] ?? inputSpec?.groundingContext;
191+
if (groundingContext) {
192+
specConfig.groundingContext = groundingContext;
182193
}
183194
}
184-
if (flags['max-topics']) {
185-
specConfig.maxNumOfTopics = flags['max-topics'];
195+
const maxNumOfTopics = flags['max-topics'] ?? validateMaxTopics(inputSpec?.maxNumOfTopics);
196+
if (maxNumOfTopics) {
197+
specConfig.maxNumOfTopics = maxNumOfTopics;
186198
}
199+
// Should we log the specConfig being used? It's returned in the JSON and the generated spec.
200+
// this.log(`${ansis.green(figures.tick)} ${ansis.bold(message)} ${ansis.cyan(valueFromFlag)}`);
187201
const agentSpec = await agent.createSpecV2(specConfig);
188202

189-
// create the directory if not already created
190-
mkdirSync(join(flags['output-dir']), { recursive: true });
191-
192-
// Write a yaml file with the returned job specs
193-
const filePath = join(flags['output-dir'], flags['file-name']);
194-
writeFileSync(filePath, YAML.stringify(agentSpec));
203+
const outputFilePath = writeSpecFile(flags['output-file'], agentSpec);
195204

196205
this.spinner.stop();
197206

198-
this.log(`\nSaved agent spec: ${filePath}`);
207+
this.log(`\nSaved agent spec: ${outputFilePath}`);
199208

200-
return { ...{ isSuccess: true, specPath: filePath }, ...agentSpec };
209+
return { ...{ isSuccess: true, specPath: outputFilePath }, ...agentSpec };
201210
}
211+
}
202212

203-
/**
204-
* Get a flag value or prompt the user for a value.
205-
*
206-
* Resolution order:
207-
* - Flag value provided by the user
208-
* - Prompt the user for a value
209-
*/
210-
public async getFlagOrPrompt(valueFromFlag: string | undefined, flagDef: FlaggablePrompt): Promise<string> {
211-
const message = flagDef.message.replace(/\.$/, '');
213+
const promptForFlag = async (flagDef: FlaggablePrompt): Promise<string> => {
214+
const message = flagDef.message.replace(/\.$/, '');
215+
if (flagDef.options) {
216+
return select({
217+
choices: flagDef.options.map((o) => ({ name: o, value: o })),
218+
message,
219+
theme,
220+
});
221+
}
212222

213-
if (valueFromFlag) {
214-
this.log(`${ansis.green(figures.tick)} ${ansis.bold(message)} ${ansis.cyan(valueFromFlag)}`);
223+
return inquirerInput({
224+
message,
225+
validate: flagDef.validate,
226+
theme,
227+
});
228+
};
215229

216-
return valueFromFlag;
230+
const validateAgentType = (agentType?: string): string | undefined => {
231+
if (agentType) {
232+
if (!['customer', 'internal'].includes(agentType.trim())) {
233+
throw messages.createError('error.invalidAgentType', [agentType]);
217234
}
235+
return agentType.trim();
236+
}
237+
};
218238

219-
if (flagDef.options) {
220-
return select({
221-
choices: flagDef.options.map((o) => ({ name: o, value: o })),
222-
message,
223-
theme,
224-
});
239+
const validateMaxTopics = (maxTopics?: number): number | undefined => {
240+
if (maxTopics) {
241+
if (!isNaN(maxTopics) && isFinite(maxTopics)) {
242+
if (maxTopics > 0) {
243+
return maxTopics;
244+
}
225245
}
226-
227-
return inquirerInput({
228-
message,
229-
validate: flagDef.validate,
230-
theme,
231-
});
246+
throw messages.createError('error.invalidMaxTopics', [maxTopics]);
232247
}
233-
}
248+
};
249+
250+
const writeSpecFile = (outputFile: string, agentSpec: AgentJobSpecV2): string => {
251+
// create the directory if not already created
252+
const outputFilePath = resolve(outputFile);
253+
mkdirSync(dirname(outputFilePath), { recursive: true });
254+
255+
// Write a yaml file with the returned job specs
256+
writeFileSync(outputFilePath, YAML.stringify(agentSpec));
257+
258+
return outputFilePath;
259+
};

0 commit comments

Comments
 (0)