From 480466df7cced048aedfe588b1599509b620539d Mon Sep 17 00:00:00 2001 From: Steve Hetzel Date: Sat, 18 Jan 2025 13:47:02 -0700 Subject: [PATCH] fix: bump version of core to 8.8.2 --- command-snapshot.json | 334 ++++++++++++++++--------- messages/agent.create-v2.md | 57 +++++ messages/agent.generate.spec-v2.md | 28 --- messages/shared.md | 28 +++ package.json | 4 +- schemas/agent-create.json | 6 +- schemas/agent-create__v2.json | 21 ++ schemas/agent-generate-spec.json | 6 +- schemas/agent-generate-spec__v2.json | 21 +- schemas/agent-preview.json | 2 +- schemas/agent-test-cancel.json | 7 +- schemas/agent-test-results.json | 54 +++- schemas/agent-test-resume.json | 7 +- schemas/agent-test-run.json | 7 +- src/commands/agent/create-v2.ts | 226 +++++++++++++++++ src/commands/agent/generate/spec-v2.ts | 136 +--------- src/flags.ts | 123 +++++++++ yarn.lock | 38 ++- 18 files changed, 800 insertions(+), 305 deletions(-) create mode 100644 messages/agent.create-v2.md create mode 100644 schemas/agent-create__v2.json create mode 100644 src/commands/agent/create-v2.ts diff --git a/command-snapshot.json b/command-snapshot.json index 0f9c125..75bdbed 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -1,119 +1,217 @@ [ - { - "alias": [], - "command": "agent:create", - "flagAliases": [], - "flagChars": ["f", "n", "o"], - "flags": ["api-version", "flags-dir", "json", "name", "spec", "target-org"], - "plugin": "@salesforce/plugin-agent" - }, - { - "alias": [], - "command": "agent:generate:definition", - "flagAliases": [], - "flagChars": [], - "flags": ["flags-dir"], - "plugin": "@salesforce/plugin-agent" - }, - { - "alias": [], - "command": "agent:generate:spec", - "flagAliases": [], - "flagChars": ["d", "f", "o", "t"], - "flags": [ - "api-version", - "company-description", - "company-name", - "company-website", - "file-name", - "flags-dir", - "json", - "output-dir", - "role", - "target-org", - "type" - ], - "plugin": "@salesforce/plugin-agent" - }, - { - "alias": [], - "command": "agent:generate:spec-v2", - "flagAliases": [], - "flagChars": ["o", "t"], - "flags": [ - "api-version", - "company-description", - "company-name", - "company-website", - "flags-dir", - "grounding-context", - "json", - "max-topics", - "output-file", - "prompt-template", - "role", - "spec", - "target-org", - "type" - ], - "plugin": "@salesforce/plugin-agent" - }, - { - "alias": [], - "command": "agent:generate:testset", - "flagAliases": [], - "flagChars": [], - "flags": ["flags-dir"], - "plugin": "@salesforce/plugin-agent" - }, - { - "alias": [], - "command": "agent:preview", - "flagAliases": [], - "flagChars": ["n", "o"], - "flags": ["api-version", "flags-dir", "name", "target-org"], - "plugin": "@salesforce/plugin-agent" - }, - { - "alias": [], - "command": "agent:test:cancel", - "flagAliases": [], - "flagChars": ["i", "o", "r"], - "flags": ["api-version", "flags-dir", "job-id", "json", "target-org", "use-most-recent"], - "plugin": "@salesforce/plugin-agent" - }, - { - "alias": [], - "command": "agent:test:results", - "flagAliases": [], - "flagChars": ["f", "i", "o"], - "flags": ["api-version", "flags-dir", "job-id", "json", "output-dir", "result-format", "target-org"], - "plugin": "@salesforce/plugin-agent" - }, - { - "alias": [], - "command": "agent:test:resume", - "flagAliases": [], - "flagChars": ["f", "i", "o", "r", "w"], - "flags": [ - "api-version", - "flags-dir", - "job-id", - "json", - "output-dir", - "result-format", - "target-org", - "use-most-recent", - "wait" - ], - "plugin": "@salesforce/plugin-agent" - }, - { - "alias": [], - "command": "agent:test:run", - "flagAliases": [], - "flagChars": ["f", "n", "o", "w"], - "flags": ["api-version", "flags-dir", "json", "name", "output-dir", "result-format", "target-org", "wait"], - "plugin": "@salesforce/plugin-agent" - } -] + { + "alias": [], + "command": "agent:create", + "flagAliases": [], + "flagChars": [ + "f", + "n", + "o" + ], + "flags": [ + "api-version", + "flags-dir", + "json", + "name", + "spec", + "target-org" + ], + "plugin": "@salesforce/plugin-agent" + }, + { + "alias": [], + "command": "agent:create-v2", + "flagAliases": [], + "flagChars": [ + "o" + ], + "flags": [ + "agent-api-name", + "agent-name", + "api-version", + "enrich-logs", + "flags-dir", + "json", + "planner-id", + "preview", + "primary-language", + "spec", + "target-org", + "tone", + "user-id" + ], + "plugin": "@salesforce/plugin-agent" + }, + { + "alias": [], + "command": "agent:generate:definition", + "flagAliases": [], + "flagChars": [], + "flags": [ + "flags-dir" + ], + "plugin": "@salesforce/plugin-agent" + }, + { + "alias": [], + "command": "agent:generate:spec", + "flagAliases": [], + "flagChars": [ + "d", + "f", + "o", + "t" + ], + "flags": [ + "api-version", + "company-description", + "company-name", + "company-website", + "file-name", + "flags-dir", + "json", + "output-dir", + "role", + "target-org", + "type" + ], + "plugin": "@salesforce/plugin-agent" + }, + { + "alias": [], + "command": "agent:generate:spec-v2", + "flagAliases": [], + "flagChars": [ + "o", + "t" + ], + "flags": [ + "api-version", + "company-description", + "company-name", + "company-website", + "flags-dir", + "grounding-context", + "json", + "max-topics", + "output-file", + "prompt-template", + "role", + "spec", + "target-org", + "type" + ], + "plugin": "@salesforce/plugin-agent" + }, + { + "alias": [], + "command": "agent:generate:testset", + "flagAliases": [], + "flagChars": [], + "flags": [ + "flags-dir" + ], + "plugin": "@salesforce/plugin-agent" + }, + { + "alias": [], + "command": "agent:preview", + "flagAliases": [], + "flagChars": [ + "n", + "o" + ], + "flags": [ + "api-version", + "flags-dir", + "name", + "target-org" + ], + "plugin": "@salesforce/plugin-agent" + }, + { + "alias": [], + "command": "agent:test:cancel", + "flagAliases": [], + "flagChars": [ + "i", + "o", + "r" + ], + "flags": [ + "api-version", + "flags-dir", + "job-id", + "json", + "target-org", + "use-most-recent" + ], + "plugin": "@salesforce/plugin-agent" + }, + { + "alias": [], + "command": "agent:test:results", + "flagAliases": [], + "flagChars": [ + "f", + "i", + "o" + ], + "flags": [ + "api-version", + "flags-dir", + "job-id", + "json", + "output-dir", + "result-format", + "target-org" + ], + "plugin": "@salesforce/plugin-agent" + }, + { + "alias": [], + "command": "agent:test:resume", + "flagAliases": [], + "flagChars": [ + "f", + "i", + "o", + "r", + "w" + ], + "flags": [ + "api-version", + "flags-dir", + "job-id", + "json", + "output-dir", + "result-format", + "target-org", + "use-most-recent", + "wait" + ], + "plugin": "@salesforce/plugin-agent" + }, + { + "alias": [], + "command": "agent:test:run", + "flagAliases": [], + "flagChars": [ + "f", + "n", + "o", + "w" + ], + "flags": [ + "api-version", + "flags-dir", + "json", + "name", + "output-dir", + "result-format", + "target-org", + "wait" + ], + "plugin": "@salesforce/plugin-agent" + } +] \ No newline at end of file diff --git a/messages/agent.create-v2.md b/messages/agent.create-v2.md new file mode 100644 index 0000000..41fe1fb --- /dev/null +++ b/messages/agent.create-v2.md @@ -0,0 +1,57 @@ +# summary + +Create an agent in your org from a local agent spec file. + +# description + +To generate an agent spec file, run the "agent generate spec" CLI command, which outputs a YAML file with the list of jobs and descriptions that the new agent can perform. Then specify this generated spec file to the --spec flag of this command, along with the name of the new agent. + +When this command finishes, your org contains the new agent, which you can then edit in the Agent Builder UI. The new agent already has a list of topics and actions that were automatically created from the list of jobs in the provided agent spec file. This command also retrieves all the metadata files associated with the new agent to your local DX project. + +To open the new agent in your org's Agent Builder UI, run this command: "sf org open agent --name ". + +# flags.spec.summary + +Path to an agent spec file. + +# flags.preview.summary + +Preview the agent without saving in your org. + +# flags.agent-name.summary + +Name for the new agent. + +# flags.agent-api-name.summary + +API name for the new agent. + +# flags.user-id.summary + +Custom user ID for the agent. + +# flags.enrich-logs.summary + +Adds agent conversation data to event logs. + +# flags.tone.summary + +Conversational style of agent responses. + +# flags.primary-language.summary + +Language the agent uses in conversations. + +# flags.planner-id.summary + +The GenAiPlanner ID to associate with the agent. + +# error.missingRequiredFlags + +Missing required flags: %s + +# examples + +- Create an agent called "CustomerSupportAgent" in an org with alias "my-org" using the specified agent spec file: + + <%= config.bin %> <%= command.id %> --name CustomerSupportAgent --spec ./config/agentSpec.json --target-org my-org diff --git a/messages/agent.generate.spec-v2.md b/messages/agent.generate.spec-v2.md index 88de92e..ebccc1e 100644 --- a/messages/agent.generate.spec-v2.md +++ b/messages/agent.generate.spec-v2.md @@ -10,26 +10,6 @@ An agent spec is a list of jobs and descriptions that capture what the agent can When your agent spec is ready, you then create the agent in your org by specifying the agent spec file to the --job-spec flag of the "agent create" CLI command. -# flags.type.summary - -Type of agent to create. - -# flags.role.summary - -Role of the agent. - -# flags.company-name.summary - -Name of your company. - -# flags.company-description.summary - -Description of your company. - -# flags.company-website.summary - -Website URL of your company. - # flags.output-file.summary Path for the generated agent spec file (yaml); can be an absolute or relative path. @@ -60,14 +40,6 @@ Spec file (yaml) to use as input to the command. <%= config.bin %> <%= command.id %> --output-dir specs --target-org my-org -# error.invalidAgentType - -agentType must be either "customer" or "internal". Found: [%s] - -# error.invalidMaxTopics - -maxNumOfTopics must be a number greater than 0. Found: [%s] - # error.missingRequiredFlags Missing required flags: %s diff --git a/messages/shared.md b/messages/shared.md index 605fe0c..65eb3af 100644 --- a/messages/shared.md +++ b/messages/shared.md @@ -9,3 +9,31 @@ Directory to write the agent test results into. # flags.output-dir.description If the agent test run completes, write the results to the specified directory. If the test is still running, the test results aren't written. + +# flags.type.summary + +Type of agent to create. + +# flags.role.summary + +Role of the agent. + +# flags.company-name.summary + +Name of your company. + +# flags.company-description.summary + +Description of your company. + +# flags.company-website.summary + +Website URL of your company. + +# error.invalidAgentType + +agentType must be either "customer" or "internal". Found: [%s] + +# error.invalidMaxTopics + +maxNumOfTopics must be a number greater than 0. Found: [%s] diff --git a/package.json b/package.json index 7b97b13..0282c81 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "@inquirer/select": "^4.0.1", "@oclif/core": "^4", "@oclif/multi-stage-output": "^0.7.12", - "@salesforce/agents": "^0.6.0", - "@salesforce/core": "^8.8.0", + "@salesforce/agents": "^0.7.0", + "@salesforce/core": "^8.8.2", "@salesforce/kit": "^3.2.1", "@salesforce/sf-plugins-core": "^12.1.0", "ansis": "^3.3.2", diff --git a/schemas/agent-create.json b/schemas/agent-create.json index eb28296..9196a7f 100644 --- a/schemas/agent-create.json +++ b/schemas/agent-create.json @@ -12,8 +12,10 @@ "type": "string" } }, - "required": ["isSuccess"], + "required": [ + "isSuccess" + ], "additionalProperties": false } } -} +} \ No newline at end of file diff --git a/schemas/agent-create__v2.json b/schemas/agent-create__v2.json new file mode 100644 index 0000000..9196a7f --- /dev/null +++ b/schemas/agent-create__v2.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/AgentCreateResult", + "definitions": { + "AgentCreateResult": { + "type": "object", + "properties": { + "isSuccess": { + "type": "boolean" + }, + "errorMessage": { + "type": "string" + } + }, + "required": [ + "isSuccess" + ], + "additionalProperties": false + } + } +} \ No newline at end of file diff --git a/schemas/agent-generate-spec.json b/schemas/agent-generate-spec.json index 51bb4c2..38d003f 100644 --- a/schemas/agent-generate-spec.json +++ b/schemas/agent-generate-spec.json @@ -15,8 +15,10 @@ "type": "string" } }, - "required": ["isSuccess"], + "required": [ + "isSuccess" + ], "additionalProperties": false } } -} +} \ No newline at end of file diff --git a/schemas/agent-generate-spec__v2.json b/schemas/agent-generate-spec__v2.json index 31b1786..2252b51 100644 --- a/schemas/agent-generate-spec__v2.json +++ b/schemas/agent-generate-spec__v2.json @@ -11,7 +11,10 @@ }, "agentType": { "type": "string", - "enum": ["customer", "internal"], + "enum": [ + "customer", + "internal" + ], "description": "Internal type is copilots; used by customers' employees. Customer type is agents; used by customers' customers." }, "role": { @@ -48,7 +51,14 @@ "type": "string" } }, - "required": ["agentType", "companyDescription", "companyName", "isSuccess", "role", "topics"] + "required": [ + "agentType", + "companyDescription", + "companyName", + "isSuccess", + "role", + "topics" + ] }, "DraftAgentTopics": { "type": "array", @@ -62,11 +72,14 @@ "type": "string" } }, - "required": ["name", "description"], + "required": [ + "name", + "description" + ], "additionalProperties": false }, "minItems": 1, "maxItems": 1 } } -} +} \ No newline at end of file diff --git a/schemas/agent-preview.json b/schemas/agent-preview.json index 617d82f..feb4e00 100644 --- a/schemas/agent-preview.json +++ b/schemas/agent-preview.json @@ -6,4 +6,4 @@ "type": "null" } } -} +} \ No newline at end of file diff --git a/schemas/agent-test-cancel.json b/schemas/agent-test-cancel.json index bf11239..6786c7d 100644 --- a/schemas/agent-test-cancel.json +++ b/schemas/agent-test-cancel.json @@ -18,8 +18,11 @@ "type": "string" } }, - "required": ["aiEvaluationId", "success"], + "required": [ + "aiEvaluationId", + "success" + ], "additionalProperties": false } } -} +} \ No newline at end of file diff --git a/schemas/agent-test-results.json b/schemas/agent-test-results.json index 4b0d329..a6b288b 100644 --- a/schemas/agent-test-results.json +++ b/schemas/agent-test-results.json @@ -36,16 +36,29 @@ } } }, - "required": ["name", "testCases"], + "required": [ + "name", + "testCases" + ], "additionalProperties": false } }, - "required": ["status", "startTime", "subjectName", "testSet"], + "required": [ + "status", + "startTime", + "subjectName", + "testSet" + ], "additionalProperties": false }, "TestStatus": { "type": "string", - "enum": ["NEW", "IN_PROGRESS", "COMPLETED", "ERROR"] + "enum": [ + "NEW", + "IN_PROGRESS", + "COMPLETED", + "ERROR" + ] }, "TestCaseResult": { "type": "object", @@ -80,7 +93,10 @@ }, "outcome": { "type": "string", - "enum": ["Success", "Failure"] + "enum": [ + "Success", + "Failure" + ] }, "topic": { "type": "string" @@ -92,7 +108,14 @@ "type": "string" } }, - "required": ["type", "actionsSequence", "outcome", "topic", "inputTokensCount", "outputTokensCount"], + "required": [ + "type", + "actionsSequence", + "outcome", + "topic", + "inputTokensCount", + "outputTokensCount" + ], "additionalProperties": false }, "expectationResults": { @@ -114,11 +137,17 @@ }, "result": { "type": "string", - "enum": ["Passed", "Failed"] + "enum": [ + "Passed", + "Failed" + ] }, "metricLabel": { "type": "string", - "enum": ["Accuracy", "Precision"] + "enum": [ + "Accuracy", + "Precision" + ] }, "metricExplainability": { "type": "string" @@ -154,8 +183,15 @@ } } }, - "required": ["status", "number", "utterance", "startTime", "generatedData", "expectationResults"], + "required": [ + "status", + "number", + "utterance", + "startTime", + "generatedData", + "expectationResults" + ], "additionalProperties": false } } -} +} \ No newline at end of file diff --git a/schemas/agent-test-resume.json b/schemas/agent-test-resume.json index 5f86d12..c067a17 100644 --- a/schemas/agent-test-resume.json +++ b/schemas/agent-test-resume.json @@ -12,8 +12,11 @@ "type": "string" } }, - "required": ["aiEvaluationId", "status"], + "required": [ + "aiEvaluationId", + "status" + ], "additionalProperties": false } } -} +} \ No newline at end of file diff --git a/schemas/agent-test-run.json b/schemas/agent-test-run.json index 284c121..c4408c7 100644 --- a/schemas/agent-test-run.json +++ b/schemas/agent-test-run.json @@ -12,8 +12,11 @@ "type": "string" } }, - "required": ["aiEvaluationId", "status"], + "required": [ + "aiEvaluationId", + "status" + ], "additionalProperties": false } } -} +} \ No newline at end of file diff --git a/src/commands/agent/create-v2.ts b/src/commands/agent/create-v2.ts new file mode 100644 index 0000000..98271a8 --- /dev/null +++ b/src/commands/agent/create-v2.ts @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2024, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * 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 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 { colorize } from '@oclif/core/ux'; +import { Agent, AgentJobSpecV2, AgentCreateConfigV2, AgentCreateLifecycleStagesV2 } from '@salesforce/agents'; +import { makeFlags, promptForFlag, validateAgentType } from '../../flags.js'; + +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const messages = Messages.loadMessages('@salesforce/plugin-agent', 'agent.create-v2'); + +export type AgentCreateResult = { + isSuccess: boolean; + errorMessage?: string; +}; + +const MSO_STAGES = { + parse: 'Parsing Agent spec', + preview: 'Creating Agent for preview', + create: 'Creating Agent in org', + retrieve: 'Retrieving Agent metadata', +}; + +const FLAGGABLE_PROMPTS = { + 'agent-name': { + message: messages.getMessage('flags.agent-name.summary'), + validate: (d: string): boolean | string => d.length > 0 || 'Agent Name cannot be empty', + required: true, + }, + 'user-id': { + message: messages.getMessage('flags.user-id.summary'), + validate: (d: string): boolean | string => { + // Allow empty string + if (d.length === 0) return true; + + if (d.length === 15 || d.length === 18) { + if (d.startsWith('005')) { + return true; + } + } + return 'Please enter a valid User ID (005 prefix)'; + } + }, + 'enrich-logs': { + message: messages.getMessage('flags.enrich-logs.summary'), + validate: (): boolean | string => true, + options: ['true', 'false'], + default: 'false', + }, + tone: { + message: messages.getMessage('flags.tone.summary'), + validate: (): boolean | string => true, + options: ['formal', 'casual', 'neutral'], + default: 'casual', + }, +}; + +export default class AgentCreateV2 extends SfCommand { + public static readonly summary = messages.getMessage('summary'); + public static readonly description = messages.getMessage('description'); + public static readonly examples = messages.getMessages('examples'); + public static readonly requiresProject = true; + public static state = 'beta'; + + public static readonly flags = { + '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'), + }), + // Currently hidden; Do we even want to expose this? + 'agent-api-name': Flags.string({ + summary: messages.getMessage('flags.agent-api-name.summary'), + hidden: true, + }), + // Currently hidden because only 'en_US' is supported + 'primary-language': Flags.string({ + summary: messages.getMessage('flags.primary-language.summary'), + options: ['en_US'], + default: 'en_US', + hidden: true, + }), + // Seems a very uncommon usecase, but it's possible to do it in the server side API + 'planner-id': Flags.string({ + summary: messages.getMessage('flags.planner-id.summary'), + }), + }; + + // eslint-disable-next-line complexity + public async run(): Promise { + const { flags } = await this.parse(AgentCreateV2); + + // throw error if --json is used and not all required flags are provided + if (this.jsonEnabled()) { + if (!flags.preview && !flags['agent-name']) { + throw messages.createError('error.missingRequiredFlags', ['agent-name']); + } + } + + // Read the agent spec and validate + const inputSpec = YAML.parse(readFileSync(resolve(flags.spec), 'utf8')) as Partial; + validateSpec(inputSpec); + + // If we're saving the agent and we don't have flag values, prompt. + let agentName = flags['agent-name']; + let userId = flags['user-id']; + let enrichLogs = flags['enrich-logs']; + let tone = flags.tone; + if (!this.jsonEnabled() && !flags.preview) { + agentName ??= await promptForFlag(FLAGGABLE_PROMPTS['agent-name']); + userId ??= await promptForFlag(FLAGGABLE_PROMPTS['user-id']); + enrichLogs ??= await promptForFlag(FLAGGABLE_PROMPTS['enrich-logs']); + tone ??= await promptForFlag(FLAGGABLE_PROMPTS.tone); + } + + let title: string; + const stages = [MSO_STAGES.parse]; + if (flags.preview) { + title = 'Previewing Agent Creation'; + stages.push(MSO_STAGES.preview); + } else { + title = `Creating ${agentName as string} Agent`; + stages.push(MSO_STAGES.create); + stages.push(MSO_STAGES.retrieve); + } + + const mso = new MultiStageOutput({ + jsonEnabled: this.jsonEnabled(), + title, + stages, + }); + mso.goto(MSO_STAGES.parse); + + // @ts-expect-error not using async method in callback + Lifecycle.getInstance().on(AgentCreateLifecycleStagesV2.Previewing, () => mso.goto(MSO_STAGES.preview)); + // @ts-expect-error not using async method in callback + Lifecycle.getInstance().on(AgentCreateLifecycleStagesV2.Creating, () => mso.goto(MSO_STAGES.create)); + // @ts-expect-error not using async method in callback + Lifecycle.getInstance().on(AgentCreateLifecycleStagesV2.Retrieving, () => mso.goto(MSO_STAGES.retrieve)); + + const connection = flags['target-org'].getConnection(flags['api-version']); + const agent = new Agent(connection, this.project!); + + const agentConfig: AgentCreateConfigV2 = { + agentType: inputSpec.agentType!, + generationInfo: { + defaultInfo: { + role: inputSpec.role!, + companyName: inputSpec.companyName!, + companyDescription: inputSpec.companyDescription!, + preDefinedTopics: inputSpec.topics, + } + }, + generationSettings: {}, + } + if (inputSpec?.companyWebsite) { + agentConfig.generationInfo.defaultInfo.companyWebsite = inputSpec?.companyWebsite; + } + if (!flags.preview) { + agentConfig.saveAgent = true; + agentConfig.agentSettings = { + agentName: agentName!, + } + if (flags['agent-api-name']) { + agentConfig.agentSettings.agentApiName = flags['agent-api-name']; + } + if (flags['planner-id']) { + agentConfig.agentSettings.plannerId = flags['planner-id']; + } + if (flags['user-id']) { + agentConfig.agentSettings.userId = userId; + } + agentConfig.agentSettings.enrichLogs = Boolean(enrichLogs); + agentConfig.agentSettings.tone = tone as 'casual' | 'formal' | 'neutral'; + } + const response = await agent.createV2(agentConfig); + + mso.stop(); + + if (response.isSuccess) { + if (!flags.preview) { + this.log(colorize( + 'green', + `Successfully created ${agentName as string} in ${flags['target-org'].getUsername() ?? 'the target org'}.` + )); + this.log(`Use ${colorize('dim', `sf org open agent --name ${agentName as string}`)} to view the agent in the browser.`); + } else { + const previewFileName = `agentPreview_${new Date().toISOString()}.json`; + writeFileSync(previewFileName, JSON.stringify(response, null, 2)); + this.log(colorize('green', `Successfully created agent for preview. See ${previewFileName}`)); + } + } else { + this.log(colorize('red', `failed to create agent: ${response.errorMessage ?? ''}`)); + } + + return response; + } +} + +// The spec must define: agentType, role, companyName, companyDescription, and topics. +// Agent type must be 'customer' or 'internal'. +const validateSpec = (spec: Partial): void => { + const requiredSpecValues: Array<'agentType' | 'role' | 'companyName' | 'companyDescription' | 'topics'> = + ['agentType', 'role', 'companyName', 'companyDescription', 'topics']; + const missingFlags = requiredSpecValues.filter(f => !spec[f]); + if (missingFlags.length) { + throw messages.createError('error.missingRequiredFlags', [missingFlags.join(', ')]); + } + + validateAgentType(spec.agentType, true); +} diff --git a/src/commands/agent/generate/spec-v2.ts b/src/commands/agent/generate/spec-v2.ts index 1def871..3b2d463 100644 --- a/src/commands/agent/generate/spec-v2.ts +++ b/src/commands/agent/generate/spec-v2.ts @@ -7,13 +7,10 @@ import { join, resolve, dirname } from 'node:path'; import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; -import { Messages, SfProject } from '@salesforce/core'; -import { Interfaces } from '@oclif/core'; +import { Messages } from '@salesforce/core'; import YAML from 'yaml'; -import select from '@inquirer/select'; -import inquirerInput from '@inquirer/input'; import { Agent, AgentJobSpecCreateConfigV2, AgentJobSpecV2 } from '@salesforce/agents'; -import { theme } from '../../../inquirer-theme.js'; +import { FLAGGABLE_SPEC_PROMPTS, makeFlags, promptForFlag, validateAgentType, validateMaxTopics } from '../../../flags.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-agent', 'agent.generate.spec-v2'); @@ -24,82 +21,6 @@ export type AgentCreateSpecResult = { specPath?: string; // the location of the job spec file } & AgentJobSpecV2; -type FlaggablePrompt = { - message: string; - options?: readonly string[] | string[]; - validate: (d: string) => boolean | string; - char?: Interfaces.AlphabetLowercase | Interfaces.AlphabetUppercase; - required?: boolean; -}; - -type FlagsOfPrompts> = Record< - keyof T, - Interfaces.OptionFlag ->; - -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, - }, - role: { - message: messages.getMessage('flags.role.summary'), - validate: (d: string): boolean | string => d.length > 0 || 'Role cannot be empty', - required: true, - }, - 'company-name': { - message: messages.getMessage('flags.company-name.summary'), - validate: (d: string): boolean | string => d.length > 0 || 'Company name cannot be empty', - required: true, - }, - 'company-description': { - message: messages.getMessage('flags.company-description.summary'), - validate: (d: string): boolean | string => d.length > 0 || 'Company description cannot be empty', - required: true, - }, - 'company-website': { - message: messages.getMessage('flags.company-website.summary'), - validate: (d: string): boolean | string => { - // Allow empty string - if (d.length === 0) return true; - - try { - new URL(d); - return true; - } catch (e) { - return 'Please enter a valid URL'; - } - }, - }, -} satisfies Record; - -function validateInput(input: string, validate: (input: string) => boolean | string): never | string { - const result = validate(input); - if (typeof result === 'string') throw new Error(result); - return input; -} - -function makeFlags>(flaggablePrompts: T): FlagsOfPrompts { - return Object.fromEntries( - Object.entries(flaggablePrompts).map(([key, value]) => [ - key, - Flags.string({ - summary: value.message, - options: value.options, - char: value.char, - // eslint-disable-next-line @typescript-eslint/require-await - async parse(input) { - return validateInput(input, value.validate); - }, - // NOTE: we purposely omit the required property here because we want to allow the flag to be missing in interactive mode - }), - ]) - ) as FlagsOfPrompts; -} - export default class AgentCreateSpecV2 extends SfCommand { public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); @@ -110,7 +31,7 @@ export default class AgentCreateSpecV2 extends SfCommand public static readonly flags = { 'target-org': Flags.requiredOrg(), 'api-version': Flags.orgApiVersion(), - ...makeFlags(FLAGGABLE_PROMPTS), + ...makeFlags(FLAGGABLE_SPEC_PROMPTS), spec: Flags.file({ summary: messages.getMessage('flags.spec.summary'), exists: true, @@ -138,7 +59,7 @@ export default class AgentCreateSpecV2 extends SfCommand // throw error if --json is used and not all required flags are provided if (this.jsonEnabled()) { - const missingFlags = Object.entries(FLAGGABLE_PROMPTS) + const missingFlags = Object.entries(FLAGGABLE_SPEC_PROMPTS) .filter(([key, prompt]) => 'required' in prompt && prompt.required && !(key in flags)) .map(([key]) => key); @@ -157,24 +78,24 @@ export default class AgentCreateSpecV2 extends SfCommand } // Flags override inputSpec values. Prompt if neither is set. - const type = flags.type ?? validateAgentType(inputSpec?.agentType) ?? (await promptForFlag(FLAGGABLE_PROMPTS.type)); - const role = flags.role ?? inputSpec?.role ?? (await promptForFlag(FLAGGABLE_PROMPTS.role)); + const type = flags.type ?? validateAgentType(inputSpec?.agentType) ?? (await promptForFlag(FLAGGABLE_SPEC_PROMPTS.type)); + const role = flags.role ?? inputSpec?.role ?? (await promptForFlag(FLAGGABLE_SPEC_PROMPTS.role)); const companyName = - flags['company-name'] ?? inputSpec?.companyName ?? (await promptForFlag(FLAGGABLE_PROMPTS['company-name'])); + flags['company-name'] ?? inputSpec?.companyName ?? (await promptForFlag(FLAGGABLE_SPEC_PROMPTS['company-name'])); const companyDescription = flags['company-description'] ?? inputSpec?.companyDescription ?? - (await promptForFlag(FLAGGABLE_PROMPTS['company-description'])); + (await promptForFlag(FLAGGABLE_SPEC_PROMPTS['company-description'])); const companyWebsite = flags['company-website'] ?? inputSpec?.companyWebsite ?? - (await promptForFlag(FLAGGABLE_PROMPTS['company-website'])); + (await promptForFlag(FLAGGABLE_SPEC_PROMPTS['company-website'])); this.log(); this.spinner.start('Creating agent spec'); const connection = flags['target-org'].getConnection(flags['api-version']); - const agent = new Agent(connection, this.project as SfProject); + const agent = new Agent(connection, this.project!); const specConfig: AgentJobSpecCreateConfigV2 = { agentType: type as 'customer' | 'internal', role, @@ -210,43 +131,6 @@ export default class AgentCreateSpecV2 extends SfCommand } } -const promptForFlag = async (flagDef: FlaggablePrompt): Promise => { - const message = flagDef.message.replace(/\.$/, ''); - if (flagDef.options) { - return select({ - choices: flagDef.options.map((o) => ({ name: o, value: o })), - message, - theme, - }); - } - - return inquirerInput({ - message, - validate: flagDef.validate, - theme, - }); -}; - -const validateAgentType = (agentType?: string): string | undefined => { - if (agentType) { - if (!['customer', 'internal'].includes(agentType.trim())) { - throw messages.createError('error.invalidAgentType', [agentType]); - } - return agentType.trim(); - } -}; - -const validateMaxTopics = (maxTopics?: number): number | undefined => { - if (maxTopics) { - if (!isNaN(maxTopics) && isFinite(maxTopics)) { - if (maxTopics > 0) { - return maxTopics; - } - } - throw messages.createError('error.invalidMaxTopics', [maxTopics]); - } -}; - const writeSpecFile = (outputFile: string, agentSpec: AgentJobSpecV2): string => { // create the directory if not already created const outputFilePath = resolve(outputFile); diff --git a/src/flags.ts b/src/flags.ts index 1695172..d42a835 100644 --- a/src/flags.ts +++ b/src/flags.ts @@ -4,12 +4,31 @@ * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ + +import { Interfaces } from '@oclif/core'; import { Flags } from '@salesforce/sf-plugins-core'; import { Messages } from '@salesforce/core'; +import select from '@inquirer/select'; +import inquirerInput from '@inquirer/input'; +import { theme } from './inquirer-theme.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-agent', 'shared'); +export type FlaggablePrompt = { + message: string; + options?: readonly string[] | string[]; + validate: (d: string) => boolean | string; + char?: Interfaces.AlphabetLowercase | Interfaces.AlphabetUppercase; + required?: boolean; + default?: string | boolean; +}; + +type FlagsOfPrompts> = Record< + keyof T, + Interfaces.OptionFlag +>; + export const resultFormatFlag = Flags.option({ options: ['json', 'human', 'junit', 'tap'] as const, default: 'human', @@ -21,3 +40,107 @@ export const testOutputDirFlag = Flags.custom({ description: messages.getMessage('flags.output-dir.description'), summary: messages.getMessage('flags.output-dir.summary'), }); + +export const FLAGGABLE_SPEC_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, + }, + role: { + message: messages.getMessage('flags.role.summary'), + validate: (d: string): boolean | string => d.length > 0 || 'Role cannot be empty', + required: true, + }, + 'company-name': { + message: messages.getMessage('flags.company-name.summary'), + validate: (d: string): boolean | string => d.length > 0 || 'Company name cannot be empty', + required: true, + }, + 'company-description': { + message: messages.getMessage('flags.company-description.summary'), + validate: (d: string): boolean | string => d.length > 0 || 'Company description cannot be empty', + required: true, + }, + 'company-website': { + message: messages.getMessage('flags.company-website.summary'), + validate: (d: string): boolean | string => { + // Allow empty string + if (d.length === 0) return true; + + try { + new URL(d); + return true; + } catch (e) { + return 'Please enter a valid URL'; + } + }, + }, +} satisfies Record; + +function validateInput(input: string, validate: (input: string) => boolean | string): never | string { + const result = validate(input); + if (typeof result === 'string') throw new Error(result); + return input; +} + +export function makeFlags>(flaggablePrompts: T): FlagsOfPrompts { + return Object.fromEntries( + Object.entries(flaggablePrompts).map(([key, value]) => [ + key, + Flags.string({ + summary: value.message, + options: value.options, + char: value.char, + // eslint-disable-next-line @typescript-eslint/require-await + async parse(input) { + return validateInput(input, value.validate); + }, + // NOTE: we purposely omit the required property here because we want to allow the flag to be missing in interactive mode + }), + ]) + ) as FlagsOfPrompts; +} + +export const promptForFlag = async (flagDef: FlaggablePrompt): Promise => { + const message = flagDef.message.replace(/\.$/, ''); + if (flagDef.options) { + return select({ + choices: flagDef.options.map((o) => ({ name: o, value: o })), + message, + theme, + }); + } + + return inquirerInput({ + message, + validate: flagDef.validate, + theme, + }); +}; + +export const validateAgentType = (agentType?: string, required = false): string | undefined => { + if (required && !agentType) { + throw messages.createError('error.invalidAgentType', [agentType]); + } + if (agentType) { + if (!['customer', 'internal'].includes(agentType.trim())) { + throw messages.createError('error.invalidAgentType', [agentType]); + } + return agentType.trim(); + } +}; + +export const validateMaxTopics = (maxTopics?: number): number | undefined => { + // Deliberately using: != null + if (maxTopics != null) { + if (!isNaN(maxTopics) && isFinite(maxTopics)) { + if (maxTopics > 0) { + return maxTopics; + } + } + throw messages.createError('error.invalidMaxTopics', [maxTopics]); + } +}; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 19d4d7d..40c5771 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1446,16 +1446,16 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@salesforce/agents@^0.6.0": - version "0.6.0" - resolved "https://registry.yarnpkg.com/@salesforce/agents/-/agents-0.6.0.tgz#6725cd9927bb543f2127a9f1e264ea26da61cec4" - integrity sha512-jZ1dlg56NcAQNmLMF53WJLx5dBDViGdb8uTU7VCsdnvbqcJq93xlWRZiVew3939f96nQZ4gqtEDfBi/a2RAqdQ== +"@salesforce/agents@^0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@salesforce/agents/-/agents-0.7.0.tgz#9472478e0213f1fc97a8447fa45292a6db808498" + integrity sha512-Tud1fkx5eSH6VKRz2RlnPcFjq5kLVB3lfPtod8N3wp/uXzMkcsMhTBdDFPasycxggJlPANctnpMLpInpRk3KbA== dependencies: - "@salesforce/core" "^8.8.0" + "@salesforce/core" "^8.8.2" "@salesforce/kit" "^3.2.3" "@salesforce/sf-plugins-core" "^12.1.2" "@salesforce/source-deploy-retrieve" "^12.12.3" - ansis "^3.8.1" + ansis "^3.9.0" fast-xml-parser "^4" nock "^13.5.6" @@ -1499,6 +1499,30 @@ semver "^7.6.3" ts-retry-promise "^0.8.1" +"@salesforce/core@^8.8.2": + version "8.8.2" + resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-8.8.2.tgz#0fc1632cc183b8c62e333f1f884bf3ee0f4aee6e" + integrity sha512-EOH75R2Nr2tg7b3gbyzRNwZPfZ/dZOpmMKmFHKL9oCtUI59+N4IkVljg6dpKYypVKuJJTzjI7T2ibnA8/cluZg== + dependencies: + "@jsforce/jsforce-node" "^3.6.1" + "@salesforce/kit" "^3.2.2" + "@salesforce/schemas" "^1.9.0" + "@salesforce/ts-types" "^2.0.10" + ajv "^8.17.1" + change-case "^4.1.2" + fast-levenshtein "^3.0.0" + faye "^1.4.0" + form-data "^4.0.0" + js2xmlparser "^4.0.1" + jsonwebtoken "9.0.2" + jszip "3.10.1" + pino "^9.4.0" + pino-abstract-transport "^1.2.0" + pino-pretty "^11.2.2" + proper-lockfile "^4.1.2" + semver "^7.6.3" + ts-retry-promise "^0.8.1" + "@salesforce/dev-config@^4.3.1": version "4.3.1" resolved "https://registry.yarnpkg.com/@salesforce/dev-config/-/dev-config-4.3.1.tgz#4dac8245df79d675258b50e1d24e8c636eaa5e10" @@ -2671,7 +2695,7 @@ ansis@^3.3.1, ansis@^3.3.2: resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.3.2.tgz#15adc36fea112da95c74d309706e593618accac3" integrity sha512-cFthbBlt+Oi0i9Pv/j6YdVWJh54CtjGACaMPCIrEV4Ha7HWsIjXDwseYV79TIL0B4+KfSwD5S70PeQDkPUd1rA== -ansis@^3.8.1: +ansis@^3.8.1, ansis@^3.9.0: version "3.9.0" resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.9.0.tgz#d195c93c31a333916142ff8f0be4d7e3872f262e" integrity sha512-PcDrVe15ldexeZMsVLBAzBwF2KhZgaU0R+CHxH+x5kqn/pO+UWVBZJ+NEXMPpEOLUFeNsnNdoWYc2gwO+MVkDg==