Skip to content

Commit

Permalink
Merge pull request #31 from forcedotcom/sh/create-agent-v2
Browse files Browse the repository at this point in the history
feat: add mock agentCreateV2 API and tests
  • Loading branch information
shetzel authored Jan 18, 2025
2 parents 2b1f698 + f3df889 commit cbcdd1c
Show file tree
Hide file tree
Showing 6 changed files with 322 additions and 5 deletions.
65 changes: 63 additions & 2 deletions src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
*/
Expand Down Expand Up @@ -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<AgentCreateResponseV2> {
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<AgentCreateResponseV2>('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<AgentCreateResponseV2>('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.
*
Expand All @@ -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<AgentJobSpecV2> {
Expand Down
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

export {
type AgentCreateConfig,
type AgentCreateConfigV2,
type AgentCreateResponse,
type AgentCreateResponseV2,
type AgentJobSpec,
type AgentJobSpecV2,
type AgentJobSpecCreateConfig,
Expand All @@ -18,7 +20,7 @@ export {
type DraftAgentTopicsResponse,
SfAgent,
} from './types';
export { Agent, AgentCreateLifecycleStages } from './agent';
export { Agent, AgentCreateLifecycleStages, AgentCreateLifecycleStagesV2 } from './agent';
export {
AgentTester,
convertTestResultsToFormat,
Expand Down
113 changes: 113 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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;
Expand Down
58 changes: 56 additions & 2 deletions test/agents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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)
Expand Down Expand Up @@ -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);
Expand Down
41 changes: 41 additions & 0 deletions test/mocks/createAgent-Preview/connect_ai-assist_create-agent.json
Original file line number Diff line number Diff line change
@@ -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"]
}
}
Loading

0 comments on commit cbcdd1c

Please sign in to comment.