Skip to content

Commit

Permalink
feat: add initial agent job spec create and mock
Browse files Browse the repository at this point in the history
  • Loading branch information
shetzel committed Nov 8, 2024
1 parent 57b4700 commit 0c5d8d6
Show file tree
Hide file tree
Showing 9 changed files with 1,062 additions and 2,804 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
module.exports = {
extends: ['eslint-config-salesforce-typescript', 'eslint-config-salesforce-license', 'plugin:sf-plugin/recommended'],
extends: ['eslint-config-salesforce-typescript', 'eslint-config-salesforce-license'],
root: true,
};
11 changes: 3 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,6 @@ test_session*
# generated docs
docs

# ignore sfdx-trust files
*.tgz
*.sig
package.json.bak.


npm-shrinkwrap.json

# -- CLEAN ALL
*.tsbuildinfo
.eslintcache
Expand All @@ -44,3 +36,6 @@ node_modules
# os specific files
.DS_Store
.idea
/Library

.vscode/settings.json
22 changes: 11 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@
"version": "1.0.0",
"license": "BSD-3-Clause",
"author": "Salesforce",
"main": "lib/exported",
"types": "lib/exported.d.ts",
"repository": "forcedotcom/agents",
"main": "lib/index",
"types": "lib/index.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/forcedotcom/agents.git"
},
"dependencies": {
"@salesforce/core": "^8.5.2"
"@salesforce/core": "^8.5.2",
"@salesforce/kit": "^3.2.3"
},
"devDependencies": {
"@salesforce/cli-plugins-testkit": "^5.3.8",
"@salesforce/cli-plugins-testkit": "^5.3.20",
"@salesforce/dev-scripts": "^10.2.10",
"ts-node": "^10.9.2",
"typescript": "^5.5.4"
Expand Down Expand Up @@ -105,12 +109,8 @@
},
"test": {
"dependencies": [
"test:compile",
"test:only",
"test:command-reference",
"test:deprecation-policy",
"lint",
"test:json-schema",
"test:compile",
"link-check"
]
},
Expand Down Expand Up @@ -168,4 +168,4 @@
"output": []
}
}
}
}
105 changes: 105 additions & 0 deletions src/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* 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 { join } from 'node:path';
import { readFileSync, statSync } from 'node:fs';
import { inspect } from 'node:util';
import { Connection, Logger, SfError, SfProject } from '@salesforce/core';
import { getMockDir } from './mockDir.js';
import {
type SfAgent,
type AgentCreateConfig,
type AgentJobSpec,
type AgentJobSpecCreateConfig,
type AgentJobSpecCreateResponse
} from './types.js'

export class Agent implements SfAgent {
private logger: Logger;
private mockDir?: string;

public constructor(private connection: Connection, private project: SfProject) {
this.logger = Logger.childFromRoot(this.constructor.name);
this.mockDir = getMockDir();
}

public async create(config: AgentCreateConfig): Promise<void> {
this.logger.debug(`Creating Agent using config: ${inspect(config)} in project: ${this.project.getPath()}`);
// Generate a GenAiPlanner in the local project and deploy

// make API request to /services/data/{api-version}/connect/attach-agent-topics

// on success, retrieve all Agent metadata
}

/**
* Create an agent spec from provided data.
*
* @param config The configuration used to generate an agent spec.
*/
public async createSpec(config: AgentJobSpecCreateConfig): Promise<AgentJobSpec> {
this.verifyAgentSpecConfig(config);

let agentSpec: AgentJobSpec;

if (this.mockDir) {
const specFileName = `${config.name}Spec.json`;
const specFilePath = join(this.mockDir, `${specFileName}`);
try {
this.logger.debug(`Using mock directory: ${this.mockDir} for agent job spec creation`);
statSync(specFilePath);
} catch(err) {
throw SfError.create({
name: 'MissingMockFile',
message: `SF_MOCK_DIR [${this.mockDir}] must contain a spec file with name ${specFileName}`,
cause: err,
});
}
try {
this.logger.debug(`Returning mock agent spec file: ${specFilePath}`);
agentSpec = JSON.parse(readFileSync(specFilePath, 'utf8')) as AgentJobSpec;
} catch(err) {
throw SfError.create({
name: 'InvalidMockFile',
message: `SF_MOCK_DIR [${this.mockDir}] must contain a valid spec file with name ${specFileName}`,
cause: err,
actions: [
'Check that the file is readable',
'Check that the file is a valid JSON array of jobTitle and jobDescription objects'
]
});
}
} else {
// TODO: We'll probably want to wrap this for better error handling but let's see
// what it looks like first.
const response = await this.connection.requestGet<AgentJobSpecCreateResponse>(this.buildAgentJobSpecUrl(config), {
retry: { maxRetries: 3 }
});
if (response.isSuccess) {
agentSpec = response?.jobSpecs as AgentJobSpec;
} else {
throw SfError.create({
name: 'AgentJobSpecCreateError',
message: response.errorMessage ?? 'unknown',
});
}
}

return agentSpec;
}

private verifyAgentSpecConfig(config: AgentJobSpecCreateConfig): void {
// TBD: for now just return. At some point verify all required config values.
if (config) return;
}

private buildAgentJobSpecUrl(config: AgentJobSpecCreateConfig): string {
const { type, role, companyName, companyDescription, companyWebsite } = config;
const website = companyWebsite ? `&${companyWebsite}` : '';
return `/connect/agent-job-spec?${type}&${role}&${companyName}&${companyDescription}${website}`;
}
}
8 changes: 7 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,10 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

export default {};
export {
AgentCreateConfig as AgentConfig,
AgentJobSpec,
AgentJobSpecCreateConfig as AgentJobSpecConfig,
AgentJobSpecCreateResponse as AgentJobSpecResponse,
SfAgent,
} from './types';
45 changes: 45 additions & 0 deletions src/mockDir.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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 { type Stats, statSync } from 'node:fs';
import { SfError } from '@salesforce/core';
import { env } from '@salesforce/kit';

/**
* If the `SF_MOCK_DIR` environment variable is set, resolve to an absolue path
* and ensure the directory exits, then return the path.
*
* NOTE: THIS SHOULD BE MOVED TO SOME OTHER LIBRARY LIKE `@salesforce/kit`.
*
* @returns the absolute path to an existing directory used for mocking behavior
*/
export const getMockDir = (): string | undefined => {
const mockDir = env.getString('SF_MOCK_DIR');
if (mockDir) {
let mockDirStat: Stats;
try {
mockDirStat = statSync(resolve(mockDir));
} catch (err) {
throw SfError.create({
name: 'InvalidMockDir',
message: `SF_MOCK_DIR [${mockDir}] not found`,
cause: err,
actions: ['If you\'re trying to mock agent behavior you must create the mock directory and add expected mock files to it.']
});
}

if (!mockDirStat.isDirectory()) {
throw SfError.create({
name: 'InvalidMockDir',
message: `SF_MOCK_DIR [${mockDir}] is not a directory`,
actions: ['If you\'re trying to mock agent behavior you must create the mock directory and add expected mock files to it.']
});
}
return mockDir;
}
}
53 changes: 53 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* 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
*/

/**
* An agent spec is a list of job titles and descriptions
* to be performed by the agent.
*/
export type AgentJobSpec = [{
jobTitle: string;
jobDescription: string;
}];

/**
* The parameters needed to generate an agent spec.
*/
export type AgentJobSpecCreateConfig = {
name: string;
type: 'customer_facing' | 'employee_facing';
role: string;
companyName: string;
companyDescription: string;
companyWebsite?: string;
}

/**
* The parameters needed to generate an agent in an org.
*
* NOTE: This is likely to change with planned serverside APIs.
*/
export type AgentCreateConfig = AgentJobSpecCreateConfig & {
spec: AgentJobSpec;
}

/**
* An interface for working with Agents.
*/
export type SfAgent = {
create(config: AgentCreateConfig): Promise<void>;
createSpec(config: AgentJobSpecCreateConfig): Promise<AgentJobSpec>;
}

/**
* The response from the `agent-job-spec` API.
*/
export type AgentJobSpecCreateResponse = {
isSuccess: boolean;
errorMessage?: string;
jobSpecs?: AgentJobSpec;
}
31 changes: 31 additions & 0 deletions test/agentJobSpecCreate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2023, 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 { expect } from 'chai';
import { MockTestOrgData, TestContext } from '@salesforce/core/testSetup';
import { SfProject } from '@salesforce/core';
import { Agent } from '../src/agent.js';

describe('agent job spec create test', () => {

const $$ = new TestContext();
const testOrg = new MockTestOrgData();
$$.inProject(true);

it('runs agent run test', async () => {
const connection = await testOrg.getConnection();
const sfProject = SfProject.getInstance();
const agent = new Agent(connection, sfProject);
const output = agent.createSpec({
name: 'MyFirstAgent',
type: 'customer_facing',
role: 'answer questions about vacation rentals',
companyName: 'Coral Cloud Enterprises',
companyDescription: 'Provide vacation rentals and activities',
});
expect(output).to.be.ok;
});
});
Loading

0 comments on commit 0c5d8d6

Please sign in to comment.