diff --git a/src/agent.ts b/src/agent.ts index 09bbc3d..433a2e0 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -14,7 +14,9 @@ import { Duration } from '@salesforce/kit'; import { type SfAgent, type AgentCreateConfig, + type AgentCreateConfigV2, type AgentCreateResponse, + type AgentCreateResponseV2, type AgentJobSpec, type AgentJobSpecV2, type AgentJobSpecCreateConfig, @@ -41,6 +43,12 @@ export const AgentCreateLifecycleStages = { RetrievingMetadata: 'retrievingmetadata', }; +export const AgentCreateLifecycleStagesV2 = { + Creating: 'creatingAgent', + Previewing: 'previewingAgent', + Retrieving: 'retrievingAgent', +}; + /** * Class for creating Agents and agent specs. */ @@ -137,6 +145,61 @@ export class Agent implements SfAgent { return response; } + /** + * Creates an agent from a configuration, optionally saving the agent in an org. + * + * @param config a configuration for creating or previewing an agent + * @returns + */ + public async createV2(config: AgentCreateConfigV2): Promise { + const url = '/connect/ai-assist/create-agent'; + + // When previewing agent creation just return the response. + if (!config.saveAgent) { + this.logger.debug( + `Previewing agent creation using config: ${inspect(config)} in project: ${this.project.getPath()}` + ); + await Lifecycle.getInstance().emit(AgentCreateLifecycleStagesV2.Previewing, {}); + return this.maybeMock.request('POST', url, config); + } + + // When saving agent creation we need to retrieve the created metadata. + this.logger.debug(`Creating agent using config: ${inspect(config)} in project: ${this.project.getPath()}`); + await Lifecycle.getInstance().emit(AgentCreateLifecycleStagesV2.Creating, {}); + const response = await this.maybeMock.request('POST', url, config); + + await Lifecycle.getInstance().emit(AgentCreateLifecycleStagesV2.Retrieving, {}); + + // + // When retrieving all agent metadata by a Bot API name works in SDR we can use that. + // + + // Query for the Bot API name by the Bot ID. + // const botApiName = this.connection.singleRecordQuery('get bot from response.agentId?.botId'); + // const cs = await ComponentSetBuilder.build({ + // metadata: { + // metadataEntries: [`Bot:${}`], + // directoryPaths: [this.project.getDefaultPackage().path], + // } + // }); + // const retrieve = await cs.retrieve({ + // usernameOrConnection: this.connection, + // merge: true, + // format: 'source', + // output: this.project.getDefaultPackage().path ?? 'force-app', + // }); + // const retrieveResult = await retrieve.pollStatus({ + // frequency: Duration.milliseconds(200), + // timeout: Duration.minutes(5), + // }); + + // if (!retrieveResult.response.success) { + // throw new SfError(`Unable to retrieve ${retrieveResult.response.id}`); + // } + + return response; + } + /** * Create an agent spec from provided data. * @@ -162,8 +225,6 @@ export class Agent implements SfAgent { /** * Create an agent spec from provided data. * - * V2 API: /connect/ai-assist/draft-agent-topics - * * @param config The configuration used to generate an agent spec. */ public async createSpecV2(config: AgentJobSpecCreateConfigV2): Promise { diff --git a/src/index.ts b/src/index.ts index 5d05bdc..2201422 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,9 @@ export { type AgentCreateConfig, + type AgentCreateConfigV2, type AgentCreateResponse, + type AgentCreateResponseV2, type AgentJobSpec, type AgentJobSpecV2, type AgentJobSpecCreateConfig, @@ -18,7 +20,7 @@ export { type DraftAgentTopicsResponse, SfAgent, } from './types'; -export { Agent, AgentCreateLifecycleStages } from './agent'; +export { Agent, AgentCreateLifecycleStages, AgentCreateLifecycleStagesV2 } from './agent'; export { AgentTester, humanFormat, diff --git a/src/types.ts b/src/types.ts index 491a5b2..3ba1362 100644 --- a/src/types.ts +++ b/src/types.ts @@ -82,6 +82,72 @@ export type AgentCreateConfig = AgentJobSpecCreateConfig & { jobSpec: AgentJobSpec; }; +export type AgentCreateConfigV2 = DraftAgentTopicsBody & { + generationInfo: { + defaultInfo: { + /** + * List of topics from an agent spec. + */ + preDefinedTopics?: DraftAgentTopics; + }; + }; + /** + * Whether to persist the agent creation in the org (true) or preview + * what would be created (false). + * + * Default: false + */ + saveAgent?: boolean; + + /** + * Settings for the agent being created. Needed only when saveAgent=true + */ + agentSettings?: { + /** + * The name to use for the Agent metadata to be created. + */ + agentName: string; + /** + * The API name to use for the Agent metadata to be created. + */ + agentApiName?: string; + /** + * The GenAiPlanner metadata ID if already created in the org. + */ + plannerId?: string; + /** + * User ID of an existing user. + * + * Determines what this agent can access and do. If your agent uses + * features or objects that require additional permissions, assign + * a custom user. + */ + userId?: string; + /** + * Store conversation transcripts, including end-user data, in event logs + * for this agent for troubleshooting. If false, conversation data is + * replaced with, "Sensitive data not available." + * + * Default: false + */ + enrichLogs?: boolean; + /** + * The conversational style of your agent's responses. Can be one of: + * formal, casual, or neutral. + * + * Default: casual + */ + tone?: 'casual' | 'formal' | 'neutral'; + /** + * The language your agent uses in conversations. Agent currently + * supports English only. + * + * Default: en_US + */ + primaryLanguage?: 'en_US'; + }; +}; + /** * The request body to send to the `draft-agent-topics` API. */ @@ -129,6 +195,53 @@ export type AgentCreateResponse = { errorMessage?: string; }; +export type AgentCreateResponseV2 = { + isSuccess: boolean; + errorMessage?: string; + /** + * If the agent was created with saveAgent=true, these are the + * IDs that make up an agent; Bot, BotVersion, and GenAiPlanner metadata. + */ + agentId?: { + botId: string; + botVersionId: string; + plannerId: string; + }; + agentDefinition: { + agentDescription: string; + topics: [ + { + scope: string; + topic: string; + actions: [ + { + actionName: string; + exampleOutput: string; + actionDescription: string; + inputs: [ + { + inputName: string; + inputDataType: string; + inputDescription: string; + } + ]; + outputs: [ + { + outputName: string; + outputDataType: string; + outputDescription: string; + } + ]; + } + ]; + instructions: string[]; + classificationDescription: string; + } + ]; + sampleUtterances: string[]; + }; +}; + export type DraftAgentTopics = [ { name: string; diff --git a/test/agents.test.ts b/test/agents.test.ts index fedb35e..409ad41 100644 --- a/test/agents.test.ts +++ b/test/agents.test.ts @@ -4,11 +4,12 @@ * 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 { join } from 'node:path'; import { expect } from 'chai'; import { MockTestOrgData, TestContext } from '@salesforce/core/testSetup'; import { Connection, SfProject } from '@salesforce/core'; import { Agent } from '../src/agent'; -import type { AgentJobSpecCreateConfig } from '../src/types'; +import type { AgentJobSpecCreateConfig, AgentCreateConfigV2 } from '../src/types'; describe('Agents', () => { const $$ = new TestContext(); @@ -18,7 +19,7 @@ describe('Agents', () => { beforeEach(async () => { $$.inProject(true); testOrg = new MockTestOrgData(); - process.env.SF_MOCK_DIR = 'test/mocks'; + process.env.SF_MOCK_DIR = join('test', 'mocks'); connection = await testOrg.getConnection(); connection.instanceUrl = 'https://mydomain.salesforce.com'; // restore the connection sandbox so that it doesn't override the builtin mocking (MaybeMock) @@ -63,6 +64,59 @@ describe('Agents', () => { expect(output.topics[0]).to.have.property('name', 'Guest_Experience_Enhancement'); }); + it('createV2 save agent', async () => { + process.env.SF_MOCK_DIR = join('test', 'mocks', 'createAgent-Save'); + const sfProject = SfProject.getInstance(); + const agent = new Agent(connection, sfProject); + const config: AgentCreateConfigV2 = { + agentType: 'customer', + saveAgent: true, + agentSettings: { + agentName: 'My First Agent', + agentApiName: 'My_First_Agent', + userId: 'new', + }, + generationInfo: { + defaultInfo: { + role: 'answer questions about vacation rentals', + companyName: 'Coral Cloud Enterprises', + companyDescription: 'Provide vacation rentals and activities', + }, + }, + generationSettings: { + maxNumOfTopics: 10, + }, + }; + const response = await agent.createV2(config); + expect(response).to.have.property('isSuccess', true); + expect(response).to.have.property('agentId'); + expect(response).to.have.property('agentDefinition'); + }); + + it('createV2 preview agent', async () => { + process.env.SF_MOCK_DIR = join('test', 'mocks', 'createAgent-Preview'); + const sfProject = SfProject.getInstance(); + const agent = new Agent(connection, sfProject); + const config: AgentCreateConfigV2 = { + agentType: 'customer', + saveAgent: false, + generationInfo: { + defaultInfo: { + role: 'answer questions about vacation rentals', + companyName: 'Coral Cloud Enterprises', + companyDescription: 'Provide vacation rentals and activities', + }, + }, + generationSettings: { + maxNumOfTopics: 10, + }, + }; + const response = await agent.createV2(config); + expect(response).to.have.property('isSuccess', true); + expect(response).to.not.have.property('agentId'); + expect(response).to.have.property('agentDefinition'); + }); + it('create', async () => { const sfProject = SfProject.getInstance(); const agent = new Agent(connection, sfProject); diff --git a/test/mocks/createAgent-Preview/connect_ai-assist_create-agent.json b/test/mocks/createAgent-Preview/connect_ai-assist_create-agent.json new file mode 100644 index 0000000..0f954da --- /dev/null +++ b/test/mocks/createAgent-Preview/connect_ai-assist_create-agent.json @@ -0,0 +1,41 @@ +{ + "isSuccess": true, + "agentDefinition": { + "agentDescription": "some agent description", + "topics": [ + { + "scope": "some scope", + "topic": "topic_name", + "actions": [ + { + "actionName": "some_action_name", + "exampleOutput": "some example output", + "actionDescription": "some description", + "inputs": [ + { + "inputName": "input_name_1", + "inputDataType": "string", + "inputDescription": "some description" + }, + { + "inputName": "input_name_2", + "inputDataType": "date", + "inputDescription": "some description" + } + ], + "outputs": [ + { + "outputName": "output_name", + "outputDataType": "string", + "outputDescription": "some description" + } + ] + } + ], + "instructions": ["instruction 1", "instruction 2"], + "classificationDescription": "some classification description" + } + ], + "Sample_Utterances": ["sample utterance 1", "sample utterance 2", "sample utterance 3"] + } +} diff --git a/test/mocks/createAgent-Save/connect_ai-assist_create-agent.json b/test/mocks/createAgent-Save/connect_ai-assist_create-agent.json new file mode 100644 index 0000000..e3e9d0f --- /dev/null +++ b/test/mocks/createAgent-Save/connect_ai-assist_create-agent.json @@ -0,0 +1,46 @@ +{ + "isSuccess": true, + "agentId": { + "botId": "12345", + "botVersionId": "12345", + "plannerId": "12345" + }, + "agentDefinition": { + "agentDescription": "some agent description", + "topics": [ + { + "scope": "some scope", + "topic": "topic_name", + "actions": [ + { + "actionName": "some_action_name", + "exampleOutput": "some example output", + "actionDescription": "some description", + "inputs": [ + { + "inputName": "input_name_1", + "inputDataType": "string", + "inputDescription": "some description" + }, + { + "inputName": "input_name_2", + "inputDataType": "date", + "inputDescription": "some description" + } + ], + "outputs": [ + { + "outputName": "output_name", + "outputDataType": "string", + "outputDescription": "some description" + } + ] + } + ], + "instructions": ["instruction 1", "instruction 2"], + "classificationDescription": "some classification description" + } + ], + "Sample_Utterances": ["sample utterance 1", "sample utterance 2", "sample utterance 3"] + } +}