Skip to content

Commit

Permalink
Fix deployment config for AI command (#137)
Browse files Browse the repository at this point in the history
Fixes issue #132
  • Loading branch information
SketchingDev authored Dec 2, 2023
1 parent c6d190e commit 83ff883
Show file tree
Hide file tree
Showing 14 changed files with 146 additions and 36 deletions.
3 changes: 2 additions & 1 deletion packages/genesys-web-messaging-tester-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ Start by setting up an API key for ChatGPT:
Write a scenario file containing all the scenarios you wish to run along with
the [ID and region of your Web Messenger Deployment](https://help.mypurecloud.com/articles/deploy-messenger/).

The scenarios are written as ChatGPT Prompts, these can take some fine-tuning to get right ([see examples here](https://genesys-messenger-tester.makingchatbots.com/writing-tests/ai/example-prompts.html)).
The scenarios are written as ChatGPT Prompts, these can take some fine-tuning to get
right ([see examples here](https://genesys-messenger-tester.makingchatbots.com/writing-tests/ai/example-prompts.html)).
The `terminatingPhrases` section defines the phrases you instruct ChatGPT to say to pass or fail a test.

> [examples/cli-ai-tests/example.yml](https://github.com/ovotech/genesys-web-messaging-tester/tree/main/examples/cli-ai-tests/example.yml)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { readFileSync } from 'fs';
import { Command } from 'commander';
import { AiTestCommandDependencies } from '../../../src/commands/aiTest/createAiTestCommand';
import { createCli } from '../../../src/createCli';
import { OpenAI } from 'openai';

describe('Session Config', () => {
let fsReadFileSync: jest.MockedFunction<typeof readFileSync>;

let webMessengerSessionFactory: jest.Mocked<
AiTestCommandDependencies['webMessengerSessionFactory']
>;
let conversationFactory: jest.Mocked<AiTestCommandDependencies['conversationFactory']>;
let mockOpenApiChatCompletions: jest.Mocked<Pick<OpenAI.Chat.Completions, 'create'>>;

let cli: Command;

beforeEach(() => {
fsReadFileSync = jest.fn();

const webMessengerSession = { on: jest.fn(), close: jest.fn() };
webMessengerSessionFactory = jest.fn().mockReturnValue(webMessengerSession);

const conversation = { waitForConversationToStart: jest.fn(), sendText: jest.fn() };
conversationFactory = jest.fn().mockReturnValue(conversation);

const cliCommand = new Command().exitOverride(() => {
throw new Error('CLI Command errored');
});

const scenarioTestCommand = new Command().exitOverride(() => {
throw new Error('Scenario Test Command errored');
});

cli = createCli(cliCommand, undefined, {
command: scenarioTestCommand,
fsReadFileSync,
fsAccessSync: jest.fn(),
webMessengerSessionFactory,
openAiChatCompletionFactory: () => mockOpenApiChatCompletions,
conversationFactory,
processEnv: { OPENAI_API_KEY: 'test' },
});
});

test('session config in Test Script loaded', async () => {
fsReadFileSync.mockReturnValue(`
config:
deploymentId: test-deployment-id-1
region: test-region-1
origin: test-origin-1
scenarios:
Test:
setup:
prompt: Test prompt
terminatingPhrases:
pass: ["PASS"]
fail: ["FAIL"]
`);
const completion: OpenAI.Chat.ChatCompletion = {
choices: [{ message: { role: 'system', content: 'PASS' }, finish_reason: 'stop', index: 0 }],
created: 0,
id: '',
model: '',
object: '',
};

mockOpenApiChatCompletions = { create: jest.fn().mockResolvedValue(completion) };

await cli.parseAsync([...['node', '/path/to/cli'], 'ai', ...['/test/path']]);

expect(webMessengerSessionFactory).toHaveBeenCalledWith({
deploymentId: 'test-deployment-id-1',
region: 'test-region-1',
origin: 'test-origin-1',
});
});

test('session config not necessary if session config args provided', async () => {
fsReadFileSync.mockReturnValue(`
scenarios:
Test:
setup:
prompt: Test prompt
terminatingPhrases:
pass: ["PASS"]
fail: ["FAIL"]
`);

const completion: OpenAI.Chat.ChatCompletion = {
choices: [{ message: { role: 'system', content: 'PASS' }, finish_reason: 'stop', index: 0 }],
created: 0,
id: '',
model: '',
object: '',
};

mockOpenApiChatCompletions = {
create: jest.fn().mockResolvedValue(completion),
};

await cli.parseAsync([
...['node', '/path/to/cli'],
'ai',
...['--deployment-id', 'test-deployment-id-2'],
...['--region', 'test-region-2'],
...['--origin', 'test-origin-2'],
...['/test/path'],
]);

expect(webMessengerSessionFactory).toHaveBeenCalledWith({
deploymentId: 'test-deployment-id-2',
region: 'test-region-2',
origin: 'test-origin-2',
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import {
import stripAnsi from 'strip-ansi';
import getPort from 'get-port';
import WebSocket from 'ws';
import { waitForMs } from './fixtures/wait';
import { createCli } from '../lib/createCli';
import { waitForMs } from '../../fixtures/wait';
import { createCli } from '../../../src/createCli';

jest.setTimeout(50000);
describe('Retry unordered message', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { accessSync, readFileSync } from 'fs';
import { Command } from 'commander';
import { Conversation, WebMessengerSession } from '@ovotech/genesys-web-messaging-tester';
import stripAnsi from 'strip-ansi';
import { createCli } from '../../src/createCli';
import { createCli } from '../../../src/createCli';

describe('Test script YAML loaded', () => {
let capturedOutput: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ScriptedTestCommandDependencies } from '../../../src/commands/scriptedTest/createScriptedTestCommand';
import { ScriptedTestCommandDependencies } from '../../../../src/commands/scriptedTest/createScriptedTestCommand';
import { readFileSync } from 'fs';
import { Command } from 'commander';
import { createCli } from '../../../src/createCli';
import { createCli } from '../../../../src/createCli';
import { ReorderedMessageDelayer } from '@ovotech/genesys-web-messaging-tester';

describe('Session Config', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { readFileSync } from 'fs';
import { Command } from 'commander';
import stripAnsi from 'strip-ansi';
import { createCli } from '../../../src/createCli';
import { createCli } from '../../../../src/createCli';

describe('Session Config Validated', () => {
let capturedOutput: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Command } from 'commander';
import { when } from 'jest-when';
import { Conversation, WebMessengerSession } from '@ovotech/genesys-web-messaging-tester';
import stripAnsi from 'strip-ansi';
import { createCli } from '../../../../src/createCli';
import { createCli } from '../../../../../src/createCli';

describe('Test script YAML loaded', () => {
const validScenarioFilePath = '/test/path/config.json';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { readFileSync } from 'fs';
import { Command } from 'commander';
import stripAnsi from 'strip-ansi';
import { createCli } from '../../../../src/createCli';
import { createCli } from '../../../../../src/createCli';

describe('Scenario Section Validated', () => {
let capturedOutput: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { accessSync, readFileSync } from 'fs';
import { Command } from 'commander';
import { createCli } from '../../../src/createCli';
import { createCli } from '../../../../src/createCli';

describe('Test-script read from disk', () => {
let fsReadFileSync: jest.MockedFunction<typeof readFileSync>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { readFileSync } from 'fs';
import { Command } from 'commander';
import stripAnsi from 'strip-ansi';
import { createCli } from '../../../src/createCli';
import { createCli } from '../../../../src/createCli';

describe('YAML file Validated', () => {
let capturedOutput: {
Expand Down
2 changes: 1 addition & 1 deletion packages/genesys-web-messaging-tester-cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ovotech/genesys-web-messaging-tester-cli",
"version": "2.0.0",
"version": "2.0.1",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"license": "Apache-2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
WebMessengerGuestSession,
WebMessengerSession,
} from '@ovotech/genesys-web-messaging-tester';
import OpenAI, { ClientOptions } from 'openai';
import { ClientOptions, OpenAI } from 'openai';
import { validateOpenAiEnvVariables } from './validateOpenAIEnvVariables';
import { Ui } from './ui';
import { validateSessionConfig } from './validateSessionConfig';
Expand All @@ -32,9 +32,10 @@ const temperature = 0.6;
export interface AiTestCommandDependencies {
command?: Command;
ui?: Ui;
openAiApiFactory?: (config: ClientOptions) => OpenAI;
openAiChatCompletionFactory?: (config: ClientOptions) => Pick<OpenAI.Chat.Completions, 'create'>;
webMessengerSessionFactory?: (sessionConfig: SessionConfig) => WebMessengerSession;
conversationFactory?: (session: WebMessengerSession) => Conversation;
processEnv?: NodeJS.ProcessEnv;
fsReadFileSync?: typeof readFileSync;
fsAccessSync?: typeof accessSync;
chatGptModel?: OpenAI.CompletionCreateParams['model'];
Expand All @@ -43,9 +44,10 @@ export interface AiTestCommandDependencies {
export function createAiTestCommand({
command = new Command(),
ui = new Ui(),
openAiApiFactory = (config) => new OpenAI(config),
openAiChatCompletionFactory = (config) => new OpenAI(config).chat.completions,
webMessengerSessionFactory = (config) => new WebMessengerGuestSession(config, { IsTest: 'true' }),
conversationFactory = (session) => new Conversation(session),
processEnv = process.env,
fsReadFileSync = readFileSync,
fsAccessSync = accessSync,
chatGptModel = 'gpt-3.5-turbo',
Expand Down Expand Up @@ -75,23 +77,11 @@ export function createAiTestCommand({
options: { deploymentId?: string; region?: string; origin?: string },
) => {
const outputConfig = command.configureOutput();
if (!outputConfig) {
throw new Error('Output must be defined');
if (!outputConfig?.writeOut || !outputConfig?.writeErr) {
throw new Error('No writeOut and/or writeErr');
}

if (!outputConfig.writeOut || !outputConfig.writeErr) {
throw new Error('No writeOut');
}

const sessionValidationResult = validateSessionConfig(options);
if (!sessionValidationResult.validSessionConfig) {
outputConfig.writeErr(
ui.validatingOpenAiEnvValidationFailed(sessionValidationResult.error),
);
throw new CommandExpectedlyFailedError();
}

const openAiEnvValidationResult = validateOpenAiEnvVariables(process.env);
const openAiEnvValidationResult = validateOpenAiEnvVariables(processEnv);
if (!openAiEnvValidationResult.openAikey) {
outputConfig.writeErr(
ui.validatingOpenAiEnvValidationFailed(openAiEnvValidationResult.error),
Expand Down Expand Up @@ -141,9 +131,11 @@ export function createAiTestCommand({

const scenario = Object.entries(validPromptScript?.scenarios)[0][1];

const session = webMessengerSessionFactory(sessionValidationResult.validSessionConfig);
const session = webMessengerSessionFactory(
sessionConfigValidationResults.validSessionConfig,
);

const openai = openAiApiFactory({
const openaiChatCompletion = openAiChatCompletionFactory({
apiKey: openAiEnvValidationResult.openAikey,
maxRetries: 5,
});
Expand All @@ -166,7 +158,7 @@ export function createAiTestCommand({
hasEnded: false,
};
do {
const { choices } = await openai.chat.completions.create({
const { choices } = await openaiChatCompletion.create({
model: chatGptModel,
n: 1, // Number of choices
temperature,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import OpenAI from 'openai';
import { OpenAI } from 'openai';
import { containsTerminatingPhrases } from './containsTerminatingPhrases';

interface Reason {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@ GENESYSCLOUD_OAUTHCLIENT_SECRET`,
},
) => {
const outputConfig = command.configureOutput();
if (!outputConfig.writeOut || !outputConfig.writeErr) {
throw new Error('No writeOut');
if (!outputConfig?.writeOut || !outputConfig?.writeErr) {
throw new Error('No writeOut and/or writeErr');
}

let associateId: { enabled: false } | { enabled: true; client: MessageIdToConvoIdClient };
Expand Down

0 comments on commit 83ff883

Please sign in to comment.