From dc998c0d8e2a791b7ade66225a589073da83049b Mon Sep 17 00:00:00 2001 From: Lily Du Date: Fri, 12 Jan 2024 10:03:54 -0800 Subject: [PATCH] completed OpenAIModerator and AzureContentSafetyModerator tests --- .../AzureContentSafetyModerator.spec.ts | 793 ++++++++---------- .../moderators/AzureContentSafetyModerator.ts | 4 +- .../src/moderators/OpenAIModerator.spec.ts | 703 +++++++--------- 3 files changed, 644 insertions(+), 856 deletions(-) diff --git a/js/packages/teams-ai/src/moderators/AzureContentSafetyModerator.spec.ts b/js/packages/teams-ai/src/moderators/AzureContentSafetyModerator.spec.ts index 0b14ac6a4..f217f8c57 100644 --- a/js/packages/teams-ai/src/moderators/AzureContentSafetyModerator.spec.ts +++ b/js/packages/teams-ai/src/moderators/AzureContentSafetyModerator.spec.ts @@ -1,482 +1,401 @@ -/* -import axios, { AxiosError } from 'axios'; +import axios from 'axios'; import { strict as assert } from 'assert'; -import { restore, stub } from 'sinon'; -import { TurnContext, CloudAdapter } from 'botbuilder'; - -import { AzureContentSafetyModerator } from './AzureContentSafetyModerator'; -import { TurnStateEntry } from '../TurnState'; +import sinon, { SinonSandbox, SinonStub } from 'sinon'; +import { TestAdapter } from 'botbuilder'; +import { AzureContentSafetyModerator, AzureOpenAIModeratorOptions } from './AzureContentSafetyModerator'; import { AI } from '../AI'; -import { - CreateContentSafetyResponse, - CreateModerationResponseResultsInner, - ModerationResponse, - ModerationSeverity, - OpenAIClientResponse -} from '../internals'; +import { Plan, PredictedDoCommand, PredictedSayCommand } from '../planners'; +import { TestTurnState } from '../TestTurnState'; describe('AzureContentSafetyModerator', () => { - afterEach(() => { - restore(); - }); - - describe('reviewPrompt', () => { - const adapter = new CloudAdapter(); - const planner = new AzureOpenAIPlanner({ - apiKey: 'test', - defaultModel: 'gpt-3.5-turbo', - endpoint: 'https://test' - }); - - const promptManager = new DefaultPromptManager({ - promptsFolder: '' - }); - - const historyOptions: AIHistoryOptions = { - assistantHistoryType: 'text', - assistantPrefix: 'Assistant:', - lineSeparator: '\n', - maxTokens: 1000, - maxTurns: 3, - trackHistory: true, - userPrefix: 'User:' - }; - - const state: DefaultTurnState = { - conversation: new TurnStateEntry({}), - user: new TurnStateEntry({}), - temp: new TurnStateEntry({ - history: '', - input: '', - output: '', - authTokens: {} - }) - }; - - const promptTemplate: PromptTemplate = { - text: '', - config: { - type: 'completion', - schema: 1, - description: '', - completion: { - frequency_penalty: 0, - max_tokens: 0, - presence_penalty: 0, - temperature: 0, - top_p: 0 - } + const mockAxios = axios; + let sinonSandbox: SinonSandbox; + let createStub: SinonStub; + let inputModerator: AzureContentSafetyModerator; + let outputModerator: AzureContentSafetyModerator; + const adapter = new TestAdapter(); + const inputOptions: AzureOpenAIModeratorOptions = { + apiKey: 'test', + moderate: 'input', + endpoint: 'https://test.com', + organization: 'stub', + apiVersion: '2023-03-15-preview', + model: 'gpt-3.5-turbo', + categories: [ + { + category: 'Hate', + severity: 2 + } + ] + }; + const outputOptions: AzureOpenAIModeratorOptions = { + apiKey: 'test', + moderate: 'output', + endpoint: 'https://test.com', + organization: 'stub', + apiVersion: '2023-03-15-preview', + model: 'gpt-3.5-turbo', + categories: [ + { + category: 'SelfHarm', + severity: 2 + }, + { + category: 'Violence', + severity: 2 + }, + { + category: 'Sexual', + severity: 2 } - }; + ] + }; + + beforeEach(() => { + sinonSandbox = sinon.createSandbox(); + createStub = sinonSandbox.stub(axios, 'create').returns(mockAxios); + inputModerator = new AzureContentSafetyModerator(inputOptions); + outputModerator = new AzureContentSafetyModerator(outputOptions); + }); - it('should return plan with action `HttpErrorActionName` when `code` >= 400', async () => { - stub(axios, 'create').returns({ - post() { - throw new AxiosError('bad request', '400'); - } - } as any); + afterEach(() => { + sinonSandbox.restore(); + }); - const moderator = new AzureContentSafetyModerator({ + describe('constructor', () => { + it('creates a moderator with options.categories defined', () => { + const options: AzureOpenAIModeratorOptions = { apiKey: 'test', moderate: 'both', - endpoint: 'https://test' - }); - - const plan = await moderator.reviewPrompt( - new TurnContext(adapter, { text: 'test' }), - state, - promptTemplate, - { - history: historyOptions, - moderator: moderator, - planner: planner, - promptManager: promptManager - } - ); - - assert.deepEqual(plan, { - type: 'plan', - commands: [ + endpoint: 'https://test.com', + organization: 'stub', + model: 'gpt-3.5-turbo', + categories: [ { - type: 'DO', - action: AI.HttpErrorActionName, - entities: { - code: '400', - message: 'bad request' - } + category: 'Hate', + severity: 2 } ] - }); + }; + const moderator = new AzureContentSafetyModerator(options); + + assert.equal(createStub.called, true); + assert.notEqual(moderator, undefined); + assert.equal(moderator.options.apiKey, options.apiKey); + assert.equal(moderator.options.apiVersion, options.apiVersion); + assert.equal(moderator.options.endpoint, options.endpoint); + assert.equal(moderator.options.model, options.model); + assert.equal(moderator.options.moderate, options.moderate); + assert.equal(moderator.options.organization, options.organization); }); - it('should throw when non-Axios error', async () => { - stub(axios, 'create').returns({ - post() { - throw new Error('something went wrong'); - } - } as any); - - const moderator = new AzureContentSafetyModerator({ + it('creates a moderator with options.categories undefined', () => { + const options: AzureOpenAIModeratorOptions = { apiKey: 'test', moderate: 'both', - endpoint: 'https://test' - }); - - const res = moderator.reviewPrompt(new TurnContext(adapter, { text: 'test' }), state, promptTemplate, { - history: historyOptions, - moderator: moderator, - planner: planner, - promptManager: promptManager - }); - - await assert.rejects(res); - }); - - it('should return plan with action `FlaggedInputActionName` when input is flagged', async () => { - const result: CreateContentSafetyResponse = { - blocklistsMatchResults: [], - hateResult: { - category: 'Hate', - severity: ModerationSeverity.Medium - }, - selfHarmResult: { - category: 'SelfHarm', - severity: ModerationSeverity.Safe - }, - sexualResult: { - category: 'Sexual', - severity: ModerationSeverity.Safe - }, - violenceResult: { - category: 'Violence', - severity: ModerationSeverity.High - } + endpoint: 'https://test.com', + organization: 'stub', + apiVersion: '2023-03-15-preview', + model: 'gpt-3.5-turbo' }; + const moderator = new AzureContentSafetyModerator(options); + + assert.equal(createStub.called, true); + assert.notEqual(moderator, undefined); + assert.equal(moderator.options.apiKey, options.apiKey); + assert.equal(moderator.options.apiVersion, options.apiVersion); + assert.equal(moderator.options.endpoint, options.endpoint); + assert.equal(moderator.options.model, options.model); + assert.equal(moderator.options.moderate, options.moderate); + assert.equal(moderator.options.organization, options.organization); + }); + }); - stub(axios, 'create').returns({ - post() { - return { - headers: {}, - status: 200, - statusText: 'ok', - data: result - } as OpenAIClientResponse; - } - } as any); - - const moderator = new AzureContentSafetyModerator({ - apiKey: 'test', - moderate: 'both', - endpoint: 'https://test' - }); - - const plan = await moderator.reviewPrompt( - new TurnContext(adapter, { text: 'test' }), - state, - promptTemplate, - { - history: historyOptions, - moderator: moderator, - planner: planner, - promptManager: promptManager - } + describe('reviewInput', () => { + it('reviews input where result exists and is flagged', async () => { + sinonSandbox.stub(mockAxios, 'post').returns( + Promise.resolve({ + status: '200', + statusText: 'OK', + data: { + hateResult: { + category: 'Hate', + severity: 1 + } + } + }) ); - - assert.deepEqual(plan, { - type: 'plan', - commands: [ - { - type: 'DO', - action: AI.FlaggedInputActionName, - entities: { - flagged: true, - categories: { - hate: true, - 'hate/threatening': true, - 'self-harm': false, - sexual: false, - 'sexual/minors': false, - violence: true, - 'violence/graphic': true - }, - category_scores: { - hate: 0.6666666666666666, - 'hate/threatening': 0.6666666666666666, - 'self-harm': 0, - sexual: 0, - 'sexual/minors': 0, - violence: 1, - 'violence/graphic': 1 - } - } as CreateModerationResponseResultsInner + await adapter.sendTextToBot('test', async (context) => { + context.activity.text = 'Hate, hate, hate'; + const state = await TestTurnState.create(context, { + temp: { a: 'foo' } + }); + const plan = await inputModerator.reviewInput(context, state); + + assert.notEqual(plan, undefined); + assert.equal(plan?.type, 'plan'); + assert.equal(plan.commands.length, 1); + assert.deepEqual(plan.commands[0], { + type: 'DO', + action: AI.FlaggedInputActionName, + parameters: { + flagged: true, + categories: { + hate: true, + 'hate/threatening': true, + 'self-harm': false, + sexual: false, + 'sexual/minors': false, + violence: false, + 'violence/graphic': false + }, + category_scores: { + hate: 0.16666666666666666, + 'hate/threatening': 0.16666666666666666, + 'self-harm': 0, + sexual: 0, + 'sexual/minors': 0, + violence: 0, + 'violence/graphic': 0 + } } - ] + } as PredictedDoCommand); }); }); - it('should return `undefined` when input is not flagged', async () => { - const result: CreateContentSafetyResponse = { - blocklistsMatchResults: [], - hateResult: { - category: 'Hate', - severity: ModerationSeverity.Safe - }, - selfHarmResult: { - category: 'SelfHarm', - severity: ModerationSeverity.Safe - }, - sexualResult: { - category: 'Sexual', - severity: ModerationSeverity.Safe - }, - violenceResult: { - category: 'Violence', - severity: ModerationSeverity.Safe - } - }; - - stub(axios, 'create').returns({ - post() { - return { - headers: {}, - status: 200, - statusText: 'ok', - data: result - } as OpenAIClientResponse; - } - } as any); - - const moderator = new AzureContentSafetyModerator({ - apiKey: 'test', - moderate: 'both', - endpoint: 'https://test' + it('reviews input where result exists and is not flagged', async () => { + sinonSandbox.stub(mockAxios, 'post').returns( + Promise.resolve({ + status: '200', + statusText: 'OK', + data: { + hateResult: { + category: 'Hate', + severity: 7 + } + } + }) + ); + await adapter.sendTextToBot('test', async (context) => { + context.activity.text = 'Neutral'; + const state = await TestTurnState.create(context, { + temp: { a: 'foo' } + }); + const plan = await inputModerator.reviewInput(context, state); + + assert.equal(plan, undefined); }); + }); - const plan = await moderator.reviewPrompt( - new TurnContext(adapter, { text: 'test' }), - state, - promptTemplate, - { - history: historyOptions, - moderator: moderator, - planner: planner, - promptManager: promptManager - } + it('reviews input where result does not exist', async () => { + sinonSandbox.stub(mockAxios, 'post').returns( + Promise.resolve({ + status: '200', + statusText: 'OK' + }) ); - - assert.equal(plan, undefined); + await adapter.sendTextToBot('test', async (context) => { + context.activity.text = 'Neutral'; + const state = await TestTurnState.create(context, { + temp: { a: 'foo' } + }); + const plan = await inputModerator.reviewInput(context, state); + + assert.notEqual(plan, undefined); + assert.equal(plan?.type, 'plan'); + assert.equal(plan.commands.length, 1); + assert.deepEqual(plan.commands[0], { + type: 'DO', + action: AI.HttpErrorActionName, + parameters: {} + } as PredictedDoCommand); + }); }); }); - describe('reviewPlan', () => { - const adapter = new CloudAdapter(); - const state: DefaultTurnState = { - conversation: new TurnStateEntry({}), - user: new TurnStateEntry({}), - temp: new TurnStateEntry({ - history: '', - input: '', - output: '', - authTokens: {} - }) - }; - - it('should return plan with action `HttpErrorActionName` when `code` >= 400', async () => { - stub(axios, 'create').returns({ - post() { - throw new AxiosError('bad request', '400'); - } - } as any); - - const moderator = new AzureContentSafetyModerator({ - apiKey: 'test', - moderate: 'both', - endpoint: 'https://test' - }); - - const plan = await moderator.reviewPlan(new TurnContext(adapter, { text: 'test' }), state, { - type: 'plan', - commands: [ - { - type: 'SAY', - response: 'test' - } as PredictedSayCommand - ] - }); - - assert.deepEqual(plan, { - type: 'plan', - commands: [ - { - type: 'DO', - action: AI.HttpErrorActionName, - entities: { - code: '400', - message: 'bad request' + describe('reviewOutput', () => { + it('reviews output of single SAY command, where result exists and is flagged', async () => { + sinonSandbox.stub(mockAxios, 'post').returns( + Promise.resolve({ + status: '200', + statusText: 'OK', + data: { + selfHarmResult: { + category: 'SelfHarm', + severity: 1 + }, + violenceResult: { + category: 'Violence', + severity: 1 + }, + sexualResult: { + category: 'Sexual', + severity: 1 } } - ] + }) + ); + await adapter.sendTextToBot('test', async (context) => { + context.activity.text = 'Self harm, violence, sexual'; + const state = await TestTurnState.create(context, { + temp: { a: 'foo' } + }); + const inputPlan: Plan = { + type: 'plan', + commands: [ + { + type: 'SAY', + response: 'Self harm, violence, sexual' + } as PredictedSayCommand + ] + }; + const plan = await outputModerator.reviewOutput(context, state, inputPlan); + + assert.notEqual(plan, undefined); + assert.equal(plan?.type, 'plan'); + assert.equal(plan.commands.length, 1); + assert.deepEqual(plan.commands[0], { + type: 'DO', + action: AI.FlaggedOutputActionName, + parameters: { + flagged: true, + categories: { + hate: false, + 'hate/threatening': false, + 'self-harm': true, + sexual: true, + 'sexual/minors': true, + violence: true, + 'violence/graphic': true + }, + category_scores: { + hate: 0, + 'hate/threatening': 0, + 'self-harm': 0.16666666666666666, + sexual: 0.16666666666666666, + 'sexual/minors': 0.16666666666666666, + violence: 0.16666666666666666, + 'violence/graphic': 0.16666666666666666 + } + } + } as PredictedDoCommand); }); }); - it('should throw when non-Axios error', async () => { - stub(axios, 'create').returns({ - post() { - throw new Error('something went wrong'); - } - } as any); - - const moderator = new AzureContentSafetyModerator({ - apiKey: 'test', - moderate: 'both', - endpoint: 'https://test' - }); - - const res = moderator.reviewPlan(new TurnContext(adapter, { text: 'test' }), state, { - type: 'plan', - commands: [ - { - type: 'SAY', - response: 'test' - } as PredictedSayCommand - ] + it('reviews output of single SAY command, where result exists and is not flagged', async () => { + sinonSandbox.stub(mockAxios, 'post').returns( + Promise.resolve({ + status: '200', + statusText: 'OK', + data: { + selfHarmResult: { + category: 'SelfHarm', + severity: 7 + }, + violenceResult: { + category: 'Violence', + severity: 7 + }, + sexualResult: { + category: 'Sexual', + severity: 7 + } + } + }) + ); + await adapter.sendTextToBot('test', async (context) => { + context.activity.text = 'Self harm, violence, sexual'; + const state = await TestTurnState.create(context, { + temp: { a: 'foo' } + }); + const inputPlan: Plan = { + type: 'plan', + commands: [ + { + type: 'SAY', + response: 'Self harm, violence, sexual' + } as PredictedSayCommand + ] + }; + const plan = await outputModerator.reviewOutput(context, state, inputPlan); + + assert.deepEqual(plan, inputPlan); }); - - await assert.rejects(res); }); - it('should return plan with action `FlaggedOutputActionName` when input is flagged', async () => { - const result: CreateContentSafetyResponse = { - blocklistsMatchResults: [], - hateResult: { - category: 'Hate', - severity: ModerationSeverity.Medium - }, - selfHarmResult: { - category: 'SelfHarm', - severity: ModerationSeverity.Safe - }, - sexualResult: { - category: 'Sexual', - severity: ModerationSeverity.Safe - }, - violenceResult: { - category: 'Violence', - severity: ModerationSeverity.High - } - }; - - stub(axios, 'create').returns({ - post() { - return { - headers: {}, - status: 200, - statusText: 'ok', - data: result - } as OpenAIClientResponse; - } - } as any); - - const moderator = new AzureContentSafetyModerator({ - apiKey: 'test', - moderate: 'both', - endpoint: 'https://test' - }); - - const plan = await moderator.reviewPlan(new TurnContext(adapter, { text: 'test' }), state, { - type: 'plan', - commands: [ - { - type: 'SAY', - response: 'test' - } as PredictedSayCommand - ] - }); - - assert.deepEqual(plan, { - type: 'plan', - commands: [ - { - type: 'DO', - action: AI.FlaggedOutputActionName, - entities: { - flagged: true, - categories: { - hate: true, - 'hate/threatening': true, - 'self-harm': false, - sexual: false, - 'sexual/minors': false, - violence: true, - 'violence/graphic': true - }, - category_scores: { - hate: 0.6666666666666666, - 'hate/threatening': 0.6666666666666666, - 'self-harm': 0, - sexual: 0, - 'sexual/minors': 0, - violence: 1, - 'violence/graphic': 1 - } - } as CreateModerationResponseResultsInner - } - ] + it('reviews output of single SAY command where result does not exist', async () => { + sinonSandbox.stub(mockAxios, 'post').returns( + Promise.resolve({ + status: '200', + statusText: 'OK' + }) + ); + await adapter.sendTextToBot('test', async (context) => { + context.activity.text = ''; + const state = await TestTurnState.create(context, { + temp: { a: 'foo' } + }); + const inputPlan: Plan = { + type: 'plan', + commands: [ + { + type: 'SAY', + response: '' + } as PredictedSayCommand + ] + }; + const plan = await outputModerator.reviewOutput(context, state, inputPlan); + + assert.notEqual(plan, undefined); + assert.equal(plan?.type, 'plan'); + assert.equal(plan.commands.length, 1); + assert.deepEqual(plan.commands[0], { + type: 'DO', + action: AI.HttpErrorActionName, + parameters: {} + } as PredictedDoCommand); }); }); - it('should return `undefined` when input is not flagged', async () => { - const result: CreateContentSafetyResponse = { - blocklistsMatchResults: [], - hateResult: { - category: 'Hate', - severity: ModerationSeverity.Safe - }, - selfHarmResult: { - category: 'SelfHarm', - severity: ModerationSeverity.Safe - }, - sexualResult: { - category: 'Sexual', - severity: ModerationSeverity.Safe - }, - violenceResult: { - category: 'Violence', - severity: ModerationSeverity.Safe - } - }; - - stub(axios, 'create').returns({ - post() { - return { - headers: {}, - status: 200, - statusText: 'ok', - data: result - } as OpenAIClientResponse; - } - } as any); + it('reviews output of plan with single DO command', async () => { + sinonSandbox.stub(mockAxios, 'post').returns( + Promise.resolve({ + status: '200', + statusText: 'OK', + data: { + selfHarmResult: { + category: 'SelfHarm', + severity: 1 + }, + violenceResult: { + category: 'Violence', + severity: 1 + }, + sexualResult: { + category: 'Sexual', + severity: 1 + } + } + }) + ); + await adapter.sendTextToBot('test', async (context) => { + context.activity.text = 'Self harm, violence, sexual'; + const state = await TestTurnState.create(context, { + temp: { a: 'foo' } + }); + const inputPlan: Plan = { + type: 'plan', + commands: [ + { + type: 'DO' + } + ] + }; + const plan = await outputModerator.reviewOutput(context, state, inputPlan); - const moderator = new AzureContentSafetyModerator({ - apiKey: 'test', - moderate: 'both', - endpoint: 'https://test' + assert.notEqual(plan, undefined); + assert.deepEqual(plan, inputPlan); }); - - const input: Plan = { - type: 'plan', - commands: [ - { - type: 'SAY', - response: 'test' - } as PredictedSayCommand - ] - }; - - const plan = await moderator.reviewPlan(new TurnContext(adapter, { text: 'test' }), state, input); - - assert.deepEqual(plan, input); }); }); }); -*/ diff --git a/js/packages/teams-ai/src/moderators/AzureContentSafetyModerator.ts b/js/packages/teams-ai/src/moderators/AzureContentSafetyModerator.ts index c628be2e2..25309e68e 100644 --- a/js/packages/teams-ai/src/moderators/AzureContentSafetyModerator.ts +++ b/js/packages/teams-ai/src/moderators/AzureContentSafetyModerator.ts @@ -77,7 +77,9 @@ export class AzureContentSafetyModerator e apiKey: options.apiKey, moderate: options.moderate ?? 'both', endpoint: options.endpoint, - apiVersion: options.apiVersion + apiVersion: options.apiVersion, + organization: options.organization, + model: options.model }; super(moderatorOptions); diff --git a/js/packages/teams-ai/src/moderators/OpenAIModerator.spec.ts b/js/packages/teams-ai/src/moderators/OpenAIModerator.spec.ts index 58cc2cac7..db929f4d4 100644 --- a/js/packages/teams-ai/src/moderators/OpenAIModerator.spec.ts +++ b/js/packages/teams-ai/src/moderators/OpenAIModerator.spec.ts @@ -1,456 +1,323 @@ -/* -import axios, { AxiosError } from 'axios'; +import axios from 'axios'; import { strict as assert } from 'assert'; -import { restore, stub } from 'sinon'; -import { TurnContext, CloudAdapter } from 'botbuilder'; - -import { OpenAIModerator } from './OpenAIModerator'; -import { TurnStateEntry } from '../TurnState'; -import { AI, AIHistoryOptions } from '../AI'; -import { OpenAIPlanner } from './OpenAIPlanner'; -import { DefaultPromptManager } from './DefaultPromptManager'; -import { CreateModerationResponseResultsInner, ModerationResponse, OpenAIClientResponse } from './OpenAIClients'; -import { DefaultTurnState } from './DefaultTurnStateManager'; -import { PromptTemplate } from '../Prompts'; -import { Plan, PredictedSayCommand } from './Planner'; +import sinon, { SinonSandbox, SinonStub } from 'sinon'; +import { TestAdapter } from 'botbuilder'; +import { AI } from '../AI'; +import { Plan, PredictedDoCommand, PredictedSayCommand } from '../planners'; +import { TestTurnState } from '../TestTurnState'; +import { OpenAIModerator, OpenAIModeratorOptions } from './OpenAIModerator'; describe('OpenAIModerator', () => { - afterEach(() => { - restore(); - }); - - describe('reviewPrompt', () => { - const adapter = new CloudAdapter(); - const planner = new OpenAIPlanner({ - apiKey: 'test', - defaultModel: 'gpt-3.5-turbo' - }); - - const promptManager = new DefaultPromptManager({ - promptsFolder: '' - }); - - const historyOptions: AIHistoryOptions = { - assistantHistoryType: 'text', - assistantPrefix: 'Assistant:', - lineSeparator: '\n', - maxTokens: 1000, - maxTurns: 3, - trackHistory: true, - userPrefix: 'User:' - }; - - const state: DefaultTurnState = { - conversation: new TurnStateEntry({}), - user: new TurnStateEntry({}), - temp: new TurnStateEntry({ - history: '', - input: '', - output: '', - authTokens: {} - }) - }; - - const promptTemplate: PromptTemplate = { - text: '', - config: { - type: 'completion', - schema: 1, - description: '', - completion: { - frequency_penalty: 0, - max_tokens: 0, - presence_penalty: 0, - temperature: 0, - top_p: 0 - } - } - }; - - it('should return plan with action `HttpErrorActionName` when `code` >= 400', async () => { - stub(axios, 'create').returns({ - post() { - throw new AxiosError('bad request', '400'); - } - } as any); - - const moderator = new OpenAIModerator({ - apiKey: 'test', - moderate: 'both' - }); - - const plan = await moderator.reviewPrompt( - new TurnContext(adapter, { text: 'test' }), - state, - promptTemplate, + const mockAxios = axios; + let sinonSandbox: SinonSandbox; + let createStub: SinonStub; + let inputModerator: OpenAIModerator; + let outputModerator: OpenAIModerator; + const adapter = new TestAdapter(); + const inputOptions: OpenAIModeratorOptions = { + apiKey: 'test', + moderate: 'input', + endpoint: 'https://test.com', + organization: 'stub', + apiVersion: '2023-03-15-preview', + model: 'gpt-3.5-turbo' + }; + const outputOptions: OpenAIModeratorOptions = { + apiKey: 'test', + moderate: 'output', + endpoint: 'https://test.com', + organization: 'stub', + apiVersion: '2023-03-15-preview', + model: 'gpt-3.5-turbo' + }; + const flaggedHateResponse = { + status: '200', + statusText: 'OK', + data: { + results: [ { - history: historyOptions, - moderator: moderator, - planner: planner, - promptManager: promptManager - } - ); - - assert.deepEqual(plan, { - type: 'plan', - commands: [ - { - type: 'DO', - action: AI.HttpErrorActionName, - entities: { - code: '400', - message: 'bad request' - } + flagged: true, + categories: { + hate: true, + 'hate/threatening': true, + 'self-harm': false, + sexual: false, + 'sexual/minors': false, + violence: false, + 'violence/graphic': false + }, + category_scores: { + hate: 0.16666666666666666, + 'hate/threatening': 0.16666666666666666, + 'self-harm': 0, + sexual: 0, + 'sexual/minors': 0, + violence: 0, + 'violence/graphic': 0 } - ] - }); - }); - - it('should throw when non-Axios error', async () => { - stub(axios, 'create').returns({ - post() { - throw new Error('something went wrong'); } - } as any); - - const moderator = new OpenAIModerator({ - apiKey: 'test', - moderate: 'both' - }); - - const res = moderator.reviewPrompt(new TurnContext(adapter, { text: 'test' }), state, promptTemplate, { - history: historyOptions, - moderator: moderator, - planner: planner, - promptManager: promptManager - }); - - await assert.rejects(res); - }); - - it('should return plan with action `FlaggedInputActionName` when input is flagged', async () => { - const result: CreateModerationResponseResultsInner = { - flagged: true, - categories: { - hate: true, - 'hate/threatening': true, - 'self-harm': true, - sexual: true, - 'sexual/minors': true, - violence: true, - 'violence/graphic': true - }, - category_scores: { - hate: 0, - 'hate/threatening': 0, - 'self-harm': 0, - sexual: 0, - 'sexual/minors': 0, - violence: 0, - 'violence/graphic': 0 - } - }; - - stub(axios, 'create').returns({ - post() { - return { - headers: {}, - status: 200, - statusText: 'ok', - data: { - id: '1', - model: 'gpt-3.5-turbo', - results: [result] - } - } as OpenAIClientResponse; - } - } as any); - - const moderator = new OpenAIModerator({ - apiKey: 'test', - moderate: 'both' - }); - - const plan = await moderator.reviewPrompt( - new TurnContext(adapter, { text: 'test' }), - state, - promptTemplate, + ] + } + }; + const flaggedSelfHarmSexualViolenceResponse = { + status: '200', + statusText: 'OK', + data: { + results: [ { - history: historyOptions, - moderator: moderator, - planner: planner, - promptManager: promptManager - } - ); - - assert.deepEqual(plan, { - type: 'plan', - commands: [ - { - type: 'DO', - action: AI.FlaggedInputActionName, - entities: result + flagged: true, + categories: { + hate: false, + 'hate/threatening': false, + 'self-harm': true, + sexual: true, + 'sexual/minors': true, + violence: true, + 'violence/graphic': true + }, + category_scores: { + hate: 0, + 'hate/threatening': 0, + 'self-harm': 0.16666666666666666, + sexual: 0.16666666666666666, + 'sexual/minors': 0.16666666666666666, + violence: 0.16666666666666666, + 'violence/graphic': 0.16666666666666666 } - ] - }); - }); - - it('should return `undefined` when input is not flagged', async () => { - const result: CreateModerationResponseResultsInner = { - flagged: false, - categories: { - hate: false, - 'hate/threatening': false, - 'self-harm': false, - sexual: false, - 'sexual/minors': false, - violence: false, - 'violence/graphic': false - }, - category_scores: { - hate: 0, - 'hate/threatening': 0, - 'self-harm': 0, - sexual: 0, - 'sexual/minors': 0, - violence: 0, - 'violence/graphic': 0 } - }; - - stub(axios, 'create').returns({ - post() { - return { - headers: {}, - status: 200, - statusText: 'ok', - data: { - id: '1', - model: 'gpt-3.5-turbo', - results: [result] - } - } as OpenAIClientResponse; - } - } as any); - - const moderator = new OpenAIModerator({ - apiKey: 'test', - moderate: 'both' - }); - - const plan = await moderator.reviewPrompt( - new TurnContext(adapter, { text: 'test' }), - state, - promptTemplate, + ] + } + }; + const unflaggedResponse = { + status: '200', + statusText: 'OK', + data: { + results: [ { - history: historyOptions, - moderator: moderator, - planner: planner, - promptManager: promptManager + flagged: false, + categories: { + hate: false, + 'hate/threatening': false, + 'self-harm': false, + sexual: false, + 'sexual/minors': false, + violence: false, + 'violence/graphic': false + }, + category_scores: { + hate: 0, + 'hate/threatening': 0, + 'self-harm': 0, + sexual: 0, + 'sexual/minors': 0, + violence: 0, + 'violence/graphic': 0 + } } - ); - - assert.equal(plan, undefined); - }); + ] + } + }; + + beforeEach(() => { + sinonSandbox = sinon.createSandbox(); + createStub = sinonSandbox.stub(axios, 'create').returns(mockAxios); + inputModerator = new OpenAIModerator(inputOptions); + outputModerator = new OpenAIModerator(outputOptions); }); - describe('reviewPlan', () => { - const adapter = new CloudAdapter(); - const state: DefaultTurnState = { - conversation: new TurnStateEntry({}), - user: new TurnStateEntry({}), - temp: new TurnStateEntry({ - history: '', - input: '', - output: '', - authTokens: {} - }) - }; - - it('should return plan with action `HttpErrorActionName` when `code` >= 400', async () => { - stub(axios, 'create').returns({ - post() { - throw new AxiosError('bad request', '400'); - } - } as any); + afterEach(() => { + sinonSandbox.restore(); + }); - const moderator = new OpenAIModerator({ + describe('constructor', () => { + it('creates a moderator', () => { + const options: OpenAIModeratorOptions = { apiKey: 'test', - moderate: 'both' - }); - - const plan = await moderator.reviewPlan(new TurnContext(adapter, { text: 'test' }), state, { - type: 'plan', - commands: [ - { - type: 'SAY', - response: 'test' - } as PredictedSayCommand - ] - }); + moderate: 'both', + endpoint: 'https://test.com', + organization: 'stub', + apiVersion: '2023-03-15-preview', + model: 'gpt-3.5-turbo' + }; + const moderator = new OpenAIModerator(options); + + assert.equal(createStub.called, true); + assert.notEqual(moderator, undefined); + assert.equal(moderator.options.apiKey, options.apiKey); + assert.equal(moderator.options.apiVersion, options.apiVersion); + assert.equal(moderator.options.endpoint, options.endpoint); + assert.equal(moderator.options.model, options.model); + assert.equal(moderator.options.moderate, options.moderate); + assert.equal(moderator.options.organization, options.organization); + }); + }); - assert.deepEqual(plan, { - type: 'plan', - commands: [ - { - type: 'DO', - action: AI.HttpErrorActionName, - entities: { - code: '400', - message: 'bad request' - } - } - ] + describe('reviewInput', () => { + it('reviews input where result exists and is flagged', async () => { + sinonSandbox.stub(mockAxios, 'post').returns(Promise.resolve(flaggedHateResponse)); + await adapter.sendTextToBot('test', async (context) => { + context.activity.text = 'Hate, hate, hate'; + const state = await TestTurnState.create(context, { + temp: { a: 'foo' } + }); + const plan = await inputModerator.reviewInput(context, state); + + assert.notEqual(plan, undefined); + assert.equal(plan?.type, 'plan'); + assert.equal(plan?.commands.length, 1); + assert.deepEqual(plan?.commands[0], { + type: 'DO', + action: AI.FlaggedInputActionName, + parameters: flaggedHateResponse.data.results[0] + } as PredictedDoCommand); }); }); - it('should throw when non-Axios error', async () => { - stub(axios, 'create').returns({ - post() { - throw new Error('something went wrong'); - } - } as any); + it('reviews input where result exists and is not flagged', async () => { + sinonSandbox.stub(mockAxios, 'post').returns(Promise.resolve(unflaggedResponse)); + await adapter.sendTextToBot('test', async (context) => { + context.activity.text = 'Neutral'; + const state = await TestTurnState.create(context, { + temp: { a: 'foo' } + }); + const plan = await inputModerator.reviewInput(context, state); - const moderator = new OpenAIModerator({ - apiKey: 'test', - moderate: 'both' + assert.equal(plan, undefined); }); + }); - const res = moderator.reviewPlan(new TurnContext(adapter, { text: 'test' }), state, { - type: 'plan', - commands: [ - { - type: 'SAY', - response: 'test' - } as PredictedSayCommand - ] + it('reviews input where result does not exist', async () => { + sinonSandbox.stub(mockAxios, 'post').returns( + Promise.resolve({ + status: '200', + statusText: 'OK' + }) + ); + await adapter.sendTextToBot('test', async (context) => { + context.activity.text = ''; + const state = await TestTurnState.create(context, { + temp: { a: 'foo' } + }); + const plan = await inputModerator.reviewInput(context, state); + + assert.notEqual(plan, undefined); + assert.equal(plan?.type, 'plan'); + assert.equal(plan?.commands.length, 1); + assert.deepEqual(plan?.commands[0], { + type: 'DO', + action: AI.HttpErrorActionName, + parameters: {} + } as PredictedDoCommand); }); - - await assert.rejects(res); }); + }); - it('should return plan with action `FlaggedOutputActionName` when input is flagged', async () => { - const result: CreateModerationResponseResultsInner = { - flagged: true, - categories: { - hate: true, - 'hate/threatening': true, - 'self-harm': true, - sexual: true, - 'sexual/minors': true, - violence: true, - 'violence/graphic': true - }, - category_scores: { - hate: 0, - 'hate/threatening': 0, - 'self-harm': 0, - sexual: 0, - 'sexual/minors': 0, - violence: 0, - 'violence/graphic': 0 - } - }; - - stub(axios, 'create').returns({ - post() { - return { - headers: {}, - status: 200, - statusText: 'ok', - data: { - id: '1', - model: 'gpt-3.5-turbo', - results: [result] - } - } as OpenAIClientResponse; - } - } as any); - - const moderator = new OpenAIModerator({ - apiKey: 'test', - moderate: 'both' + describe('reviewOutput', () => { + it('reviews output of single SAY command, where result exists and is flagged', async () => { + sinonSandbox.stub(mockAxios, 'post').returns(Promise.resolve(flaggedSelfHarmSexualViolenceResponse)); + await adapter.sendTextToBot('test', async (context) => { + context.activity.text = 'Self harm, violence, sexual'; + const state = await TestTurnState.create(context, { + temp: { a: 'foo' } + }); + const inputPlan: Plan = { + type: 'plan', + commands: [ + { + type: 'SAY', + response: 'Self harm, violence, sexual' + } as PredictedSayCommand + ] + }; + const plan = await outputModerator.reviewOutput(context, state, inputPlan); + + assert.notEqual(plan, undefined); + assert.equal(plan?.type, 'plan'); + assert.equal(plan.commands.length, 1); + assert.deepEqual(plan.commands[0], { + type: 'DO', + action: AI.FlaggedOutputActionName, + parameters: flaggedSelfHarmSexualViolenceResponse.data.results[0] + } as PredictedDoCommand); }); + }); - const plan = await moderator.reviewPlan(new TurnContext(adapter, { text: 'test' }), state, { - type: 'plan', - commands: [ - { - type: 'SAY', - response: 'test' - } as PredictedSayCommand - ] + it('reviews output of single SAY command, where result exists and is not flagged', async () => { + sinonSandbox.stub(mockAxios, 'post').returns(Promise.resolve(unflaggedResponse)); + await adapter.sendTextToBot('test', async (context) => { + context.activity.text = 'Neutral'; + const state = await TestTurnState.create(context, { + temp: { a: 'foo' } + }); + const inputPlan: Plan = { + type: 'plan', + commands: [ + { + type: 'SAY', + response: 'Neutral' + } as PredictedSayCommand + ] + }; + const plan = await outputModerator.reviewOutput(context, state, inputPlan); + + assert.deepEqual(plan, inputPlan); }); + }); - assert.deepEqual(plan, { - type: 'plan', - commands: [ - { - type: 'DO', - action: AI.FlaggedOutputActionName, - entities: result - } - ] + it('reviews output of single SAY command where result does not exist', async () => { + sinonSandbox.stub(mockAxios, 'post').returns( + Promise.resolve({ + status: '200', + statusText: 'OK' + }) + ); + await adapter.sendTextToBot('test', async (context) => { + context.activity.text = ''; + const state = await TestTurnState.create(context, { + temp: { a: 'foo' } + }); + const inputPlan: Plan = { + type: 'plan', + commands: [ + { + type: 'SAY', + response: '' + } as PredictedSayCommand + ] + }; + const plan = await outputModerator.reviewOutput(context, state, inputPlan); + + assert.notEqual(plan, undefined); + assert.equal(plan?.type, 'plan'); + assert.equal(plan.commands.length, 1); + assert.deepEqual(plan.commands[0], { + type: 'DO', + action: AI.HttpErrorActionName, + parameters: {} + } as PredictedDoCommand); }); }); - it('should return `undefined` when input is not flagged', async () => { - const result: CreateModerationResponseResultsInner = { - flagged: false, - categories: { - hate: false, - 'hate/threatening': false, - 'self-harm': false, - sexual: false, - 'sexual/minors': false, - violence: false, - 'violence/graphic': false - }, - category_scores: { - hate: 0, - 'hate/threatening': 0, - 'self-harm': 0, - sexual: 0, - 'sexual/minors': 0, - violence: 0, - 'violence/graphic': 0 - } - }; - - stub(axios, 'create').returns({ - post() { - return { - headers: {}, - status: 200, - statusText: 'ok', - data: { - id: '1', - model: 'gpt-3.5-turbo', - results: [result] + it('reviews output of plan with single DO command', async () => { + sinonSandbox.stub(mockAxios, 'post').returns(Promise.resolve(flaggedSelfHarmSexualViolenceResponse)); + await adapter.sendTextToBot('test', async (context) => { + context.activity.text = 'Self harm, violence, sexual'; + const state = await TestTurnState.create(context, { + temp: { a: 'foo' } + }); + const inputPlan: Plan = { + type: 'plan', + commands: [ + { + type: 'DO' } - } as OpenAIClientResponse; - } - } as any); + ] + }; + const plan = await outputModerator.reviewOutput(context, state, inputPlan); - const moderator = new OpenAIModerator({ - apiKey: 'test', - moderate: 'both' + assert.notEqual(plan, undefined); + assert.deepEqual(plan, inputPlan); }); - - const input: Plan = { - type: 'plan', - commands: [ - { - type: 'SAY', - response: 'test' - } as PredictedSayCommand - ] - }; - - const plan = await moderator.reviewPlan(new TurnContext(adapter, { text: 'test' }), state, input); - - assert.deepEqual(plan, input); }); }); }); -*/