-
Notifications
You must be signed in to change notification settings - Fork 199
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[JS] feat: Removed Deprecated Completions Logic, Added OpenAIClient a…
…nd AzureOpenAIClient Unit Tests, Updated Response/Request Parameters (#1138) ## Linked issues closes: #1136, #54 ## Details - Updated `createEmbeddings` with missing `model` parameter - Added unit tests for `OpenAIClient` and `AzureOpenAIClient` - Fixed model bug (Previously was appending an empty string to the URL if it wasn't specified) - Added missing header `Ocp-Apim-Subscription-Key` to enable calls to Azure Content Safety - Removed deprecated `Completions` logic - [More info here](https://platform.openai.com/docs/api-reference/completions) - Updated request/response parameters (optional, new fields, and deprecated fields) - Created a [ticket ](#1137) for updating moderation parameters and do a general cleanup of the `types` file. I didn't make these changes in this PR since it'll require logic changes as well (incl in C#), and there's quite a number of fixes here already. ## Attestation Checklist - [x] My code follows the style guidelines of this project - I have checked for/fixed spelling, linting, and other errors - I have commented my code for clarity - I have made corresponding changes to the documentation (we use [TypeDoc](https://typedoc.org/) to document our code) - My changes generate no new warnings - I have added tests that validates my changes, and provides sufficient test coverage. I have tested with: - Local testing - E2E testing in Teams - New and existing unit tests pass locally with my changes
- Loading branch information
Showing
9 changed files
with
547 additions
and
291 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
207 changes: 207 additions & 0 deletions
207
js/packages/teams-ai/src/internals/AzureOpenAIClient.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,207 @@ | ||
import assert from 'assert'; | ||
import { AzureOpenAIClient, AzureOpenAIClientOptions } from './AzureOpenAIClient'; | ||
import { CreateChatCompletionRequest, CreateEmbeddingRequest, ModerationInput } from './types'; | ||
import sinon, { SinonStub } from 'sinon'; | ||
import axios from 'axios'; | ||
|
||
describe('AzureOpenAIClient', () => { | ||
const mockAxios = axios; | ||
let client: AzureOpenAIClient; | ||
let clientWithApiVersion: AzureOpenAIClient; | ||
let cognitiveServiceClient: AzureOpenAIClient; | ||
let createStub: SinonStub; | ||
|
||
const options: AzureOpenAIClientOptions = { | ||
apiKey: 'mock-key', | ||
endpoint: 'https://mock.openai.azure.com/' | ||
}; | ||
const optionsWithApiVersion: AzureOpenAIClientOptions = { | ||
apiKey: 'mock-key', | ||
endpoint: 'https://mock.openai.azure.com/', | ||
apiVersion: '2023-03-15-preview' | ||
}; | ||
const optionsMissingEndpoint: AzureOpenAIClientOptions = { | ||
apiKey: 'mock-key', | ||
endpoint: '' | ||
}; | ||
const cognitiveServiceOptions: AzureOpenAIClientOptions = { | ||
apiKey: 'mock-key', | ||
endpoint: 'https://mock-content-safety.cognitiveservices.azure.com/', | ||
apiVersion: '2023-10-01', | ||
ocpApimSubscriptionKey: 'mock-key-2' | ||
}; | ||
const header = { | ||
headers: { | ||
'Content-Type': 'application/json', | ||
'User-Agent': 'Microsoft Teams Conversational AI SDK', | ||
'api-key': `${options.apiKey}` | ||
} | ||
}; | ||
const cognitiveServiceHeader = { | ||
headers: { | ||
'Content-Type': 'application/json', | ||
'User-Agent': 'Microsoft Teams Conversational AI SDK', | ||
'api-key': `${cognitiveServiceOptions.apiKey}`, | ||
'Ocp-Apim-Subscription-Key': `${cognitiveServiceOptions.ocpApimSubscriptionKey}` | ||
} | ||
}; | ||
const chatCompletionRequest: CreateChatCompletionRequest = { | ||
model: 'gpt-35-turbo', | ||
messages: [ | ||
{ | ||
role: 'system', | ||
content: 'You are a helpful assistant.' | ||
}, | ||
{ | ||
role: 'user', | ||
content: 'Does Azure OpenAI support customer managed keys?' | ||
}, | ||
{ | ||
role: 'assistant', | ||
content: 'Yes, customer managed keys are supported by Azure OpenAI.' | ||
}, | ||
{ | ||
role: 'user', | ||
content: 'Do other Azure AI services support this too?' | ||
} | ||
] | ||
}; | ||
const chatCompletionResponse = { | ||
status: '200', | ||
statusText: 'OK', | ||
data: { object: 'chat.completion' } | ||
}; | ||
const embeddingRequest: CreateEmbeddingRequest = { | ||
model: 'text-embedding-ada-002', | ||
input: 'The food was delicious and the waiter...' | ||
}; | ||
const embeddingResponse = { | ||
status: '200', | ||
statusText: 'OK', | ||
data: { object: 'list' } | ||
}; | ||
const moderationRequest: ModerationInput = { | ||
text: 'I want to eat' | ||
}; | ||
const moderationResponse = { | ||
status: '200', | ||
statusText: 'OK', | ||
data: { | ||
blocklistsMatch: [], | ||
categoriesAnalysis: [ | ||
{ | ||
category: 'Hate', | ||
severity: 0 | ||
} | ||
] | ||
} | ||
}; | ||
|
||
beforeEach(() => { | ||
createStub = sinon.stub(axios, 'create').returns(mockAxios); | ||
client = new AzureOpenAIClient(options); | ||
clientWithApiVersion = new AzureOpenAIClient(optionsWithApiVersion); | ||
cognitiveServiceClient = new AzureOpenAIClient(cognitiveServiceOptions); | ||
}); | ||
|
||
afterEach(() => { | ||
sinon.restore(); | ||
}); | ||
|
||
describe('constructor', () => { | ||
it('should create a valid OpenAIClient with all required fields', () => { | ||
const azureOpenAIClient = new AzureOpenAIClient(options); | ||
|
||
assert.equal(createStub.called, true); | ||
assert.notEqual(azureOpenAIClient, undefined); | ||
assert.equal(azureOpenAIClient.options.apiKey, options.apiKey); | ||
}); | ||
|
||
it('should throw error due to invalid endpoint', () => { | ||
assert.throws( | ||
() => new AzureOpenAIClient(optionsMissingEndpoint), | ||
new Error(`AzureOpenAIClient initialized without an 'endpoint'.`) | ||
); | ||
}); | ||
|
||
it('should create a valid OpenAIClient with added apiVersion field', () => { | ||
const azureOpenAIClient = new AzureOpenAIClient(optionsWithApiVersion); | ||
|
||
assert.equal(createStub.called, true); | ||
assert.notEqual(azureOpenAIClient, undefined); | ||
assert.equal(azureOpenAIClient.options.apiKey, optionsWithApiVersion.apiKey); | ||
assert.equal(azureOpenAIClient.options.endpoint, optionsWithApiVersion.endpoint); | ||
assert.equal(azureOpenAIClient.options.apiVersion, optionsWithApiVersion.apiVersion); | ||
}); | ||
}); | ||
|
||
describe('createChatCompletion', () => { | ||
it('creates valid chat completion response', async () => { | ||
const postStub = sinon.stub(mockAxios, 'post').returns(Promise.resolve(chatCompletionResponse)); | ||
const url = `${options.endpoint}/openai/deployments/${chatCompletionRequest.model}/chat/completions?api-version=2023-03-15-preview`; | ||
const response = await client.createChatCompletion(chatCompletionRequest); | ||
|
||
assert.equal(postStub.calledOnce, true); | ||
assert.equal(postStub.calledOnceWith(url, chatCompletionRequest, header), true); | ||
assert.equal(response.status, 200); | ||
assert.equal(response.statusText, 'OK'); | ||
assert.notEqual(response.data, undefined); | ||
assert.equal(response.data?.object, 'chat.completion'); | ||
}); | ||
|
||
it('creates valid chat completion response, with api version specified', async () => { | ||
const postStub = sinon.stub(mockAxios, 'post').returns(Promise.resolve(chatCompletionResponse)); | ||
const url = `${optionsWithApiVersion.endpoint}/openai/deployments/${chatCompletionRequest.model}/chat/completions?api-version=${optionsWithApiVersion.apiVersion}`; | ||
const response = await clientWithApiVersion.createChatCompletion(chatCompletionRequest); | ||
|
||
assert.equal(postStub.calledOnce, true); | ||
assert.equal(postStub.calledOnceWith(url, chatCompletionRequest, header), true); | ||
assert.equal(response.status, 200); | ||
assert.equal(response.statusText, 'OK'); | ||
assert.notEqual(response.data, undefined); | ||
assert.equal(response.data?.object, 'chat.completion'); | ||
}); | ||
}); | ||
|
||
describe('createEmbedding', () => { | ||
it('creates valid embedding response', async () => { | ||
const postStub = sinon.stub(mockAxios, 'post').returns(Promise.resolve(embeddingResponse)); | ||
const url = `${options.endpoint}/openai/deployments/${embeddingRequest.model}/embeddings?api-version=2022-12-01`; | ||
const response = await client.createEmbedding(embeddingRequest); | ||
|
||
assert.equal(postStub.calledOnce, true); | ||
assert.equal(postStub.calledOnceWith(url, embeddingRequest, header), true); | ||
assert.equal(response.status, 200); | ||
assert.equal(response.statusText, 'OK'); | ||
assert.notEqual(response.data, undefined); | ||
assert.equal(response.data?.object, 'list'); | ||
}); | ||
|
||
it('creates valid embedding response with api version specified', async () => { | ||
const postStub = sinon.stub(mockAxios, 'post').returns(Promise.resolve(embeddingResponse)); | ||
const url = `${optionsWithApiVersion.endpoint}/openai/deployments/${embeddingRequest.model}/embeddings?api-version=${optionsWithApiVersion.apiVersion}`; | ||
const response = await clientWithApiVersion.createEmbedding(embeddingRequest); | ||
|
||
assert.equal(postStub.calledOnce, true); | ||
assert.equal(postStub.calledOnceWith(url, embeddingRequest, header), true); | ||
assert.equal(response.status, 200); | ||
assert.equal(response.statusText, 'OK'); | ||
assert.notEqual(response.data, undefined); | ||
assert.equal(response.data?.object, 'list'); | ||
}); | ||
}); | ||
|
||
describe('createModeration', () => { | ||
it('creates valid moderation response', async () => { | ||
const postStub = sinon.stub(mockAxios, 'post').returns(Promise.resolve(moderationResponse)); | ||
const url = `${cognitiveServiceOptions.endpoint}/contentsafety/text:analyze?api-version=${cognitiveServiceOptions.apiVersion}`; | ||
const response = await cognitiveServiceClient.createModeration(moderationRequest); | ||
|
||
assert.equal(postStub.calledOnce, true); | ||
assert.equal(postStub.calledOnceWith(url, moderationRequest, cognitiveServiceHeader), true); | ||
assert.equal(response.status, 200); | ||
assert.equal(response.statusText, 'OK'); | ||
assert.notEqual(response.data, undefined); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.