diff --git a/.vscode/launch.json b/.vscode/launch.json index caf069e..66de95b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -16,7 +16,7 @@ "ts-node/register/transpile-only", "--preserve-symlinks" ], - "args":["./src/cognigy.ts","clone","--config", "./config.json", "--forceYes"], + "args":["./src/cognigy.ts","push", "aiAgent", "Neon", "--config", "./config.json", "--forceYes"], "console": "integratedTerminal" } ] diff --git a/README.md b/README.md index bf18769..c56b7e5 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Currently supported resources (``): - Endpoints (clone, restore, push, pull, diff) - Snapshots (create) - Extensions (pull) +- AI Agents (clone, push, pull) For Endpoints, Transformers will be separately stores as TypeScript files @@ -101,6 +102,13 @@ Clones a Virtual Agent from Cognigy.AI to disk | ------ | ----- | ------ | ------- | --------------------------------------------------------------------- | | --type | -t | String | `agent` | Which type of resource to clone (`agent` stands for the full project) | +Supported resource types for clone: +- agent (default, clones everything including AI Agents) +- flows +- endpoints +- lexicons +- aiAgents + ### Command: restore `cognigy restore` @@ -289,7 +297,6 @@ Commit using the commitizen hook with semantic naming convetion promt ```bash npx cz ``` - ### Pull Requests Create PR with any kind of feature/bugfix folloving the [semantic message format](https://github.com/semantic-release/semantic-release#commit-message-format) to the develop branch. @@ -301,3 +308,4 @@ Any PRs to develop needs to be merged as squash merges. Create a PR from develop to main and do a merge commit. This will automatically trigger a new release. To make the release publish a new minor version to the npm registry, the commit message needs to follow the [semantic message format] and having at least one of the commits to main from the last release with a fix. + diff --git a/package-lock.json b/package-lock.json index 3389dbf..a44c6bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,10 @@ "license": "MIT", "dependencies": { "-": "0.0.1", - "@cognigy/rest-api-client": "0.19.0", + "@cognigy/rest-api-client": "^0.20.0", "@google-cloud/translate": "8.0.2", "@istanbuljs/nyc-config-typescript": "1.0.2", - "axios": "^1.7.8", + "axios": "1.7.8", "chalk": "4.1.2", "cheerio": "1.0.0-rc.12", "cli-progress": "3.12.0", @@ -24,7 +24,7 @@ "d3-dsv": "2.0.0", "diff": "5.0.0", "epub2": "3.0.2", - "express": "^4.21.2", + "express": "4.21.2", "form-data": "3.0.0", "gpt-3-encoder": "1.1.4", "html-to-text": "9.0.5", @@ -517,10 +517,9 @@ } }, "node_modules/@cognigy/rest-api-client": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@cognigy/rest-api-client/-/rest-api-client-0.19.0.tgz", - "integrity": "sha512-Y9da5cFs55jHDOPpGJFjeQbngQeHSASNyttf4zokJU9WpDmZ6/zV4J9H0DcWDpCPuTEA1vxX+99S6jwf9TG8Rg==", - "license": "Cognigy Proprietary License", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@cognigy/rest-api-client/-/rest-api-client-0.20.0.tgz", + "integrity": "sha512-/VUCNa7GGPU4Ui1KVV0l/H8GY2tipv3MwCOGt+AtTeAb0w8ftjg+HGIng7SnV68LD4k0Qw8TIwMWvK9UbXzm5Q==", "dependencies": { "ajv": "6.12.6", "axios": "1.7.4", @@ -10188,9 +10187,9 @@ } }, "@cognigy/rest-api-client": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@cognigy/rest-api-client/-/rest-api-client-0.19.0.tgz", - "integrity": "sha512-Y9da5cFs55jHDOPpGJFjeQbngQeHSASNyttf4zokJU9WpDmZ6/zV4J9H0DcWDpCPuTEA1vxX+99S6jwf9TG8Rg==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@cognigy/rest-api-client/-/rest-api-client-0.20.0.tgz", + "integrity": "sha512-/VUCNa7GGPU4Ui1KVV0l/H8GY2tipv3MwCOGt+AtTeAb0w8ftjg+HGIng7SnV68LD4k0Qw8TIwMWvK9UbXzm5Q==", "requires": { "ajv": "6.12.6", "axios": "1.7.4", diff --git a/package.json b/package.json index c2ad134..ac89786 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cognigy/cognigy-cli", - "version": "0.0.0-semantic-release", + "version": "1.6.0", "description": "Cognigy Command Line Interface", "main": "./build/cognigy.js", "scripts": { @@ -44,7 +44,7 @@ }, "dependencies": { "-": "0.0.1", - "@cognigy/rest-api-client": "0.19.0", + "@cognigy/rest-api-client": "^0.20.0", "@google-cloud/translate": "8.0.2", "@istanbuljs/nyc-config-typescript": "1.0.2", "axios": "1.7.8", diff --git a/src/commands/clone.ts b/src/commands/clone.ts index 21ceb38..df51aca 100644 --- a/src/commands/clone.ts +++ b/src/commands/clone.ts @@ -8,6 +8,7 @@ import { upperFirst } from '../utils/stringUtils'; import { cloneEndpoints } from '../lib/endpoints'; import { cloneFlows } from '../lib/flows'; import { cloneLexicons } from '../lib/lexicons'; +import { cloneAiAgents } from '../lib/aiagents'; /** * Clones a full Virtual Agent project to disk @@ -43,6 +44,7 @@ export const clone = async ({ resourceType = 'agent', forceYes = false }): Promi await cloneFlows(33); await cloneEndpoints(33); await cloneLexicons(33); + await cloneAiAgents(); break; case "flows": @@ -59,6 +61,11 @@ export const clone = async ({ resourceType = 'agent', forceYes = false }): Promi case "lexicon": await cloneLexicons(100); break; + + case "aiAgents": + case "aiAgent": + await cloneAiAgents(); + break; } endProgressBar(); diff --git a/src/commands/diff.ts b/src/commands/diff.ts index 7c1b482..b879bf6 100644 --- a/src/commands/diff.ts +++ b/src/commands/diff.ts @@ -2,6 +2,7 @@ import { checkAgentDir, checkProject } from '../utils/checks'; import { diffFlows } from '../lib/flows'; import { diffEndpoints } from '../lib/endpoints'; import { diffLexicons } from '../lib/lexicons'; +import { diffAiAgents } from '../lib/aiagents'; /** * Provides a diff between a resource on disk and remote @@ -29,8 +30,11 @@ export const diff = async (resourceType: string, resourceId: string, mode: strin await diffLexicons(resourceId, mode); break; + case "aiAgent": + await diffAiAgents(resourceId, mode); + break; + default: - console.log(`\n\nInvalid diff resource type ${resourceType}.`); - return; + console.log(`Resource type ${resourceType} can't be compared.`); } }; \ No newline at end of file diff --git a/src/commands/pull.ts b/src/commands/pull.ts index e787786..4325c86 100644 --- a/src/commands/pull.ts +++ b/src/commands/pull.ts @@ -9,6 +9,7 @@ import { upperFirst } from '../utils/stringUtils'; import { pullLexicon } from '../lib/lexicons'; import { pullLocales } from '../lib/locales'; import { pullExtensions } from '../lib/extensions'; +import { pullAiAgent } from '../lib/aiagents'; /** * Pushes a single resource from disk to Cognigy.AI @@ -59,6 +60,11 @@ export const pull = async ({ resourceType, resourceName, forceYes = false }): Pr case "extensions": await pullExtensions(); break; + + case 'aiAgent': + await pullAiAgent(resourceName); + break; + default: throw(new Error(`Resource type ${resourceType} can't be pulled.`)); } diff --git a/src/commands/push.ts b/src/commands/push.ts index 738fb8a..daaecec 100644 --- a/src/commands/push.ts +++ b/src/commands/push.ts @@ -6,6 +6,7 @@ import { pushEndpoint } from '../lib/endpoints'; import { checkAgentDir, checkResourceDir, checkProject } from '../utils/checks'; import { upperFirst } from '../utils/stringUtils'; import { pushLexicon } from '../lib/lexicons'; +import { pushAiAgent } from '../lib/aiagents'; /** * Pushes a single resource from disk to Cognigy.AI @@ -55,6 +56,12 @@ export const push = async ({ resourceType, resourceName, options }): Promise => { + // Ensure aiAgents directory exists + const aiAgentsDir = getAiAgentsDir(); + await checkCreateDir(aiAgentsDir); + + // Get all aiAgents + const { items: aiAgents } = await indexAll(CognigyClient.indexAiAgents)({ + "projectId": CONFIG.agent + }); + + // Add progress tracking like in endpoints.ts + const incrementPerAgent = progressIncrement / aiAgents.length; + + for (const agent of aiAgents) { + const agentDir = getAgentDir(agent.name); + + await removeCreateDir(agentDir); + await checkCreateDir(agentDir); + + const fullAiAgent = await CognigyClient.readAiAgent({ aiAgentId: agent._id }); + await fs.writeJSON(`${agentDir}/config.json`, fullAiAgent, { spaces: 2 }); + addToProgressBar(incrementPerAgent); + } +}; + +/** + * Pulls a single AI Agent or all AI Agents from Cognigy.AI + * @param resourceName optional name of the AI Agent to pull + */ +export const pullAiAgent = async (resourceName?: string): Promise => { + // Ensure aiAgents directory exists + const aiAgentsDir = getAiAgentsDir(); + await checkCreateDir(aiAgentsDir); + + if (resourceName) { + // Pull specific aiAgent + const aiAgent = await getAiAgentByName(resourceName); + const fullAiAgent = await CognigyClient.readAiAgent({ aiAgentId: aiAgent._id }); + + // Create directory for this agent + const agentDir = getAgentDir(resourceName); + await checkCreateDir(agentDir); + + // Save to disk as config.json + await fs.writeJSON(`${agentDir}/config.json`, fullAiAgent, { spaces: 2 }); + } else { + // Pull all aiAgents + const { items: aiAgents } = await indexAll(CognigyClient.indexAiAgents)({ + "projectId": CONFIG.agent + }); + + await Promise.all(aiAgents.map(async (agent) => { + const agentDir = getAgentDir(agent.name); + await checkCreateDir(agentDir); + + const fullAiAgent = await CognigyClient.readAiAgent({ aiAgentId: agent._id }); + await fs.writeJSON(`${agentDir}/config.json`, fullAiAgent, { spaces: 2 }); + })); + } +}; + +/** + * Pushes an AI Agent to Cognigy.AI + * @param resourceName name of the AI Agent to push + * @param availableProgress How much of the progress bar can be filled by this process + */ +export const pushAiAgent = async (resourceName: string, availableProgress: number = 100): Promise => { + const agentDir = getAgentDir(resourceName); + + // Read aiAgent config from disk + const aiAgentConfig = await fs.readJSON(`${agentDir}/config.json`); + + // Find existing aiAgent + const existingAgent = await getAiAgentByName(resourceName); + + if (!existingAgent) { + throw new Error(`AI Agent '${resourceName}' not found`); + } + + try { + const cleanedConfig = cleanAiAgentConfig(aiAgentConfig); + await CognigyClient.updateAiAgent({ ...cleanedConfig }); + } catch (error) { + console.error(error); + throw new Error(`Failed to update AI Agent '${resourceName}' in Cognigy.AI`); + } + + addToProgressBar(availableProgress); +}; + +/** + * Restores AI Agents back to Cognigy.AI + * @param availableProgress How much of the progress bar can be filled by this process + */ +export const restoreAiAgents = async (availableProgress: number): Promise => { + const aiAgentsDir = `${CONFIG.agentDir}/aiAgents`; + + // read aiAgent directories + const aiAgentDirectories = fs.readdirSync(aiAgentsDir); + if (!aiAgentDirectories || aiAgentDirectories.length === 0) { + console.log("No AI Agents found, aborting...\n"); + return; + } + + const incrementPerAgent = availableProgress / aiAgentDirectories.length; + + // iterate through AI Agents and push all to Cognigy.AI + for (let aiAgent of aiAgentDirectories) { + await pushAiAgent(aiAgent, incrementPerAgent); + } + return Promise.resolve(); +}; + +// Add helper function for common operations +const getAiAgentByName = async (resourceName: string) => { + const { items: aiAgents } = await indexAll(CognigyClient.indexAiAgents)({ + "projectId": CONFIG.agent + }); + const aiAgent = aiAgents.find(agent => agent.name === resourceName); + if (!aiAgent) { + throw new Error(`AI Agent '${resourceName}' not found`); + } + return aiAgent; +}; + +// Add helper for cleaning agent config +const cleanAiAgentConfig = (config: any) => { + const cleanConfig = { ...config }; + cleanConfig.aiAgentId = cleanConfig._id; + delete cleanConfig._id; + delete cleanConfig.referenceId; + delete cleanConfig.createdBy; + delete cleanConfig.createdAt; + delete cleanConfig.organisationId; + delete cleanConfig.projectReference; + delete cleanConfig.organisationReference; + delete cleanConfig.lastChanged; + delete cleanConfig.lastChangedBy; + return cleanConfig; +}; + +const AI_AGENTS_DIR = 'aiAgents'; + +const getAiAgentsDir = () => `${CONFIG.agentDir}/${AI_AGENTS_DIR}`; +const getAgentDir = (agentName: string) => `${getAiAgentsDir()}/${agentName}`; + +/** + * Compares two AI Agent JSON representations + * @param aiAgentName Name of the AI Agent to compare + * @param mode always full + */ +export const diffAiAgents = async (aiAgentName: string, mode: string = 'full'): Promise => { + try { + // check if a valid mode was selected + if (['full'].indexOf(mode) === -1) { + console.log(`Selected mode not supported for AI Agents. Supported modes:\n\n- full\n`); + process.exit(0); + } + + const spinner = new Spinner(`Comparing ${chalk.green('local')} and ${chalk.red('remote')} AI Agent resource ${aiAgentName}... %s`); + spinner.setSpinnerString('|/-\\'); + spinner.start(); + + const aiAgentDir = getAiAgentsDir(); + + // check whether AI Agent directory and config.json exist + const agentDir = getAgentDir(aiAgentName); + if (!fs.existsSync(agentDir) || !fs.existsSync(`${agentDir}/config.json`)) { + spinner.stop(); + console.log(`\nThe requested AI Agent resource (${aiAgentName}) couldn't be found ${chalk.green('locally')}. Aborting...`); + process.exit(0); + } + + // retrieve local AI Agent config + const localConfig = await fs.readJSON(`${agentDir}/config.json`); + + // retrieve remote AI Agent config + const remoteConfig = await CognigyClient.readAiAgent({ + aiAgentId: localConfig._id + }); + + // perform full comparison and output results + const diffString = jsonDiff.diffString(remoteConfig, localConfig); + + spinner.stop(); + + if (diffString) console.log(`\n\n ${diffString}`); + else console.log(`\n\nThe local and remote resource are identical.`); + + return; + } catch (err) { + console.log(err.message); + process.exit(0); + } +}; \ No newline at end of file diff --git a/src/program/program.ts b/src/program/program.ts index 1453424..9cc65c5 100644 --- a/src/program/program.ts +++ b/src/program/program.ts @@ -67,14 +67,18 @@ program .option('-y, --forceYes', 'skips warnings and overwrites all content') .option('-t, --timeout ', 'timeout for training') .description('Pushes a resource from disk to Cognigy.AI') - .action(async (resourceType, resourceName, cmdObj) => { await push({ resourceType, resourceName, options: cmdObj }); }); + .action(async (resourceType, resourceName, cmdObj) => { + await push({ resourceType, resourceName, options: cmdObj }); + }); program .command('pull [resourceName]') .option('-c, --config ', 'force the use of a specific config file') .option('-y, --forceYes', 'skips warnings and overwrites all content') .description('Pulls a resource from Cognigy.AI to disk') - .action(async (resourceType, resourceName, cmdObj) => { await pull({ resourceType, resourceName, forceYes: cmdObj.forceYes }); }); + .action(async (resourceType, resourceName, cmdObj) => { + await pull({ resourceType, resourceName, forceYes: cmdObj.forceYes }); + }); program .command('train ') diff --git a/src/spec/commands/pull.spec.ts b/src/spec/commands/pull.spec.ts index d4309f3..c6fcef7 100644 --- a/src/spec/commands/pull.spec.ts +++ b/src/spec/commands/pull.spec.ts @@ -9,6 +9,7 @@ import * as endpointsObj from "../../lib/endpoints"; import * as localesObj from "../../lib/locales"; import * as lexiconsObj from "../../lib/lexicons"; import * as extensionsObj from "../../lib/extensions"; +import * as aiagentsObj from "../../lib/aiagents"; import * as checksObj from "../../utils/checks"; describe("Pull spec command", () => { @@ -107,6 +108,22 @@ describe("Pull spec command", () => { }); }); + describe("Pull AI Agent", () => { + let pullAiAgentStub: sinon.SinonStub; + + beforeEach(() => { + pullAiAgentStub = sandbox.stub(aiagentsObj, "pullAiAgent"); + pullAiAgentStub.resolves(true); + }); + + it("Should allow pulling aiAgent resourceType with name", async () => { + await pull({ resourceType: "aiAgent", resourceName: "agent-name", forceYes: true }); + + expect(pullAiAgentStub.called).to.be.true; + expect(pullAiAgentStub.firstCall.args).to.be.deep.equal(["agent-name"]); + }); + }); + describe("Pull unrecognised resourceType", () => { it("Should throw an error", async () => { try {