Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: W-17669635 - retrieve created agents #38

Merged
merged 2 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions messages/agents.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# invalidAgentSpecConfig

Missing one or more of the required agent spec arguments: type, role, companyName, companyDescription

# missingAgentName

The "agentName" configuration property is required when saving an agent.

# agentRetrievalError

Unable to retrieve newly created Agent metadata. Due to: %s

# agentRetrievalErrorActions

Retrieve the agent metadata using the "project retrieve start" command.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"@salesforce/core": "^8.8.2",
"@salesforce/kit": "^3.2.3",
"@salesforce/sf-plugins-core": "^12.1.2",
"@salesforce/source-deploy-retrieve": "^12.14.0",
"@salesforce/source-deploy-retrieve": "^12.14.1",
"ansis": "^3.9.0",
"fast-xml-parser": "^4",
"nock": "^13.5.6"
Expand Down
101 changes: 71 additions & 30 deletions src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export class Agent implements SfAgent {
* Creates an agent from a configuration, optionally saving the agent in an org.
*
* @param config a configuration for creating or previewing an agent
* @returns
* @returns the agent definition
*/
public async createV2(config: AgentCreateConfigV2): Promise<AgentCreateResponseV2> {
const url = '/connect/ai-assist/create-agent';
Expand All @@ -163,39 +163,61 @@ export class Agent implements SfAgent {
return this.maybeMock.request<AgentCreateResponseV2>('POST', url, config);
}

// When saving agent creation we need to retrieve the created metadata.
if (!config.agentSettings?.agentName) {
throw messages.createError('missingAgentName');
}

this.logger.debug(`Creating agent using config: ${inspect(config)} in project: ${this.project.getPath()}`);
await Lifecycle.getInstance().emit(AgentCreateLifecycleStagesV2.Creating, {});
if (!config.agentSettings.agentApiName) {
config.agentSettings.agentApiName = generateAgentApiName(config.agentSettings?.agentName);
}
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}`);
// }
// When saving agent creation we need to retrieve the created metadata.
if (response.isSuccess) {
await Lifecycle.getInstance().emit(AgentCreateLifecycleStagesV2.Retrieving, {});
const defaultPackagePath = this.project.getDefaultPackage().path ?? 'force-app';
try {
const cs = await ComponentSetBuilder.build({
metadata: {
metadataEntries: [`Agent:${config.agentSettings.agentApiName}`],
directoryPaths: [defaultPackagePath],
},
org: {
username: this.connection.getUsername() as string,
exclude: [],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: exclude a few known problem types here for ~ perf ~

},
});
const retrieve = await cs.retrieve({
usernameOrConnection: this.connection,
merge: true,
format: 'source',
output: defaultPackagePath,
});
const retrieveResult = await retrieve.pollStatus({
frequency: Duration.milliseconds(200),
timeout: Duration.minutes(5),
});
if (!retrieveResult.response.success) {
const errMessages = retrieveResult.response.messages?.toString() ?? 'unknown';
const error = messages.createError('agentRetrievalError', [errMessages]);
error.actions = [messages.getMessage('agentRetrievalErrorActions')];
throw error;
}
} catch (err) {
const error = SfError.wrap(err);
if (error.name === 'AgentRetrievalError') {
throw error;
}
throw SfError.create({
name: 'AgentRetrievalError',
message: messages.getMessage('agentRetrievalError', [error.message]),
cause: error,
actions: [messages.getMessage('agentRetrievalErrorActions')],
});
}
}

return response;
}
Expand Down Expand Up @@ -555,3 +577,22 @@ export class Agent implements SfAgent {
return [genAiSourcePath, botSourcePath, botVersionSourcePath];
}
}

/**
* Generate an API name from an agent name. Matches what the UI does.
*/
export const generateAgentApiName = (agentName: string): string => {
const maxLength = 255;
let apiName = agentName;
apiName = apiName.replace(/[\W_]+/g, '_');
if (apiName.charAt(0).match(/_/i)) {
apiName = apiName.slice(1);
}
apiName = apiName
.replace(/(^\d+)/, 'X$1')
.slice(0, maxLength)
.replace(/_$/, '');
const logger = Logger.childFromRoot('Agent-GenApiName');
logger.debug(`Generated Agent API name: [${apiName}] from Agent name: [${agentName}]`);
return apiName;
};
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export {
type DraftAgentTopicsResponse,
SfAgent,
} from './types';
export { Agent, AgentCreateLifecycleStages, AgentCreateLifecycleStagesV2 } from './agent';
export { Agent, AgentCreateLifecycleStages, AgentCreateLifecycleStagesV2, generateAgentApiName } from './agent';
export {
AgentTester,
convertTestResultsToFormat,
Expand Down
45 changes: 42 additions & 3 deletions test/agents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ 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 { ComponentSetBuilder, ComponentSet, MetadataApiRetrieve } from '@salesforce/source-deploy-retrieve';
import { Agent, generateAgentApiName } from '../src/agent';
import type { AgentJobSpecCreateConfig, AgentCreateConfigV2 } from '../src/types';

describe('Agents', () => {
Expand Down Expand Up @@ -67,14 +68,27 @@ describe('Agents', () => {
it('createV2 save agent', async () => {
process.env.SF_MOCK_DIR = join('test', 'mocks', 'createAgent-Save');
const sfProject = SfProject.getInstance();

// @ts-expect-error Not the full package def
$$.SANDBOX.stub(sfProject, 'getDefaultPackage').returns({ path: 'force-app' });
const mdApiRetrieve = new MetadataApiRetrieve({
usernameOrConnection: testOrg.getMockUserInfo().Username,
output: 'nowhere',
});
const pollingStub = $$.SANDBOX.stub(mdApiRetrieve, 'pollStatus').resolves({
// @ts-expect-error Not the full response
response: { success: true },
});
const compSet = new ComponentSet();
const retrieveStub = $$.SANDBOX.stub(compSet, 'retrieve').resolves(mdApiRetrieve);
const csbStub = $$.SANDBOX.stub(ComponentSetBuilder, 'build').resolves(compSet);

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: {
Expand All @@ -91,6 +105,10 @@ describe('Agents', () => {
expect(response).to.have.property('isSuccess', true);
expect(response).to.have.property('agentId');
expect(response).to.have.property('agentDefinition');
expect(csbStub.calledOnce).to.be.true;
expect(retrieveStub.calledOnce).to.be.true;
expect(pollingStub.calledOnce).to.be.true;
expect(config.agentSettings?.agentApiName).to.equal('My_First_Agent');
});

it('createV2 preview agent', async () => {
Expand Down Expand Up @@ -137,3 +155,24 @@ describe('Agents', () => {
expect(output).to.be.ok;
});
});

describe('generateAgentApiName', () => {
it('should create valid agent API name with spaces', () => {
expect(generateAgentApiName('My Test Agent')).to.equal('My_Test_Agent');
});
it('should create valid agent API name with no spaces', () => {
expect(generateAgentApiName('MyTestAgent')).to.equal('MyTestAgent');
});
it('should create valid agent API name with beginning underscore', () => {
expect(generateAgentApiName('_My Test Agent')).to.equal('My_Test_Agent');
});
it('should create valid agent API name with multiple beginning underscores', () => {
expect(generateAgentApiName('___My Test Agent')).to.equal('My_Test_Agent');
});
it('should create valid agent API name with special characters', () => {
expect(generateAgentApiName('My ()*&^$% Test @!""; Agent')).to.equal('My_Test_Agent');
});
it('should create valid agent API name with weird spacing', () => {
expect(generateAgentApiName(' My Test Agent ')).to.equal('My_Test_Agent');
});
});
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -699,10 +699,10 @@
cli-progress "^3.12.0"
terminal-link "^3.0.0"

"@salesforce/source-deploy-retrieve@^12.14.0":
version "12.14.0"
resolved "https://registry.yarnpkg.com/@salesforce/source-deploy-retrieve/-/source-deploy-retrieve-12.14.0.tgz#04036f76301071b2188c92f70d77a138bc0d72cf"
integrity sha512-3WOQCUY0a8cNYx5/NVtaubLEgxo/vHS/7k4Kw/FEZY3ysALpPCqWk2psJQP56xsp/SDAI3lV0VpMZadrL+ryMw==
"@salesforce/source-deploy-retrieve@^12.14.1":
version "12.14.1"
resolved "https://registry.yarnpkg.com/@salesforce/source-deploy-retrieve/-/source-deploy-retrieve-12.14.1.tgz#9067388b1eb9fadda7e6ccd894e9d1bb0e8407d5"
integrity sha512-8POp6tqoeciDFAv7a40LG21aHZ2XGHvX0/ySqcjcAVhTVhVhcP0EpC+qO+SSKjB6Ocf0/6iXMo5ciU2Eq32ydw==
dependencies:
"@salesforce/core" "^8.8.2"
"@salesforce/kit" "^3.2.3"
Expand Down