Skip to content

Commit

Permalink
[JS] feat: Removed Deprecated Completions Logic, Added OpenAIClient a…
Browse files Browse the repository at this point in the history
…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
lilyydu authored Jan 9, 2024
1 parent cecc841 commit 5372e09
Show file tree
Hide file tree
Showing 9 changed files with 547 additions and 291 deletions.
3 changes: 2 additions & 1 deletion js/packages/teams-ai/src/embeddings/EmbeddingsModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@
export interface EmbeddingsModel {
/**
* Creates embeddings for the given inputs.
* @param model Name of the model to use (or deployment for Azure).
* @param inputs Text inputs to create embeddings for.
* @returns A `EmbeddingsResponse` with a status and the generated embeddings or a message when an error occurs.
*/
createEmbeddings(inputs: string | string[]): Promise<EmbeddingsResponse>;
createEmbeddings(model: string, inputs: string | string[]): Promise<EmbeddingsResponse>;
}

/**
Expand Down
3 changes: 2 additions & 1 deletion js/packages/teams-ai/src/embeddings/OpenAIEmbeddings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,14 +156,15 @@ export class OpenAIEmbeddings implements EmbeddingsModel {
* @param inputs Text inputs to create embeddings for.
* @returns A `EmbeddingsResponse` with a status and the generated embeddings or a message when an error occurs.
*/
public async createEmbeddings(inputs: string | string[]): Promise<EmbeddingsResponse> {
public async createEmbeddings(model: string, inputs: string | string[]): Promise<EmbeddingsResponse> {
if (this.options.logRequests) {
console.log(Colorize.title('EMBEDDINGS REQUEST:'));
console.log(Colorize.output(inputs));
}

const startTime = Date.now();
const response = await this.createEmbeddingRequest({
model: model,
input: inputs
});

Expand Down
3 changes: 2 additions & 1 deletion js/packages/teams-ai/src/embeddings/TestEmbeddings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,11 @@ export class TestEmbeddings implements EmbeddingsModel {

/**
* Returns a generated set of test embeddings
* @param model Name of the model to use (or deployment for Azure).
* @param inputs Input to generate embeddings for.
* @returns The generated embeddings.
*/
public createEmbeddings(inputs: string | string[]): Promise<EmbeddingsResponse> {
public createEmbeddings(model: string, inputs: string | string[]): Promise<EmbeddingsResponse> {
// Validate inputs
if (typeof inputs == 'string') {
if (inputs.trim().length == 0) {
Expand Down
207 changes: 207 additions & 0 deletions js/packages/teams-ai/src/internals/AzureOpenAIClient.spec.ts
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);
});
});
});
28 changes: 4 additions & 24 deletions js/packages/teams-ai/src/internals/AzureOpenAIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,11 @@
import {
CreateChatCompletionRequest,
CreateChatCompletionResponse,
CreateCompletionRequest,
CreateCompletionResponse,
CreateEmbeddingRequest,
CreateEmbeddingResponse,
ModerationInput,
ModerationResponse,
OpenAICreateChatCompletionRequest,
OpenAICreateCompletionRequest,
OpenAICreateEmbeddingRequest
} from './types';
import { OpenAIClient, OpenAIClientOptions, OpenAIClientResponse } from './OpenAIClient';
Expand Down Expand Up @@ -52,20 +49,11 @@ export class AzureOpenAIClient extends OpenAIClient {
}
}

public createCompletion(request: CreateCompletionRequest): Promise<OpenAIClientResponse<CreateCompletionResponse>> {
const clone = Object.assign({}, request) as OpenAICreateCompletionRequest;
const deployment = this.removeModel(clone);
const endpoint = (this.options as AzureOpenAIClientOptions).endpoint;
const apiVersion = (this.options as AzureOpenAIClientOptions).apiVersion ?? '2022-12-01';
const url = `${endpoint}/openai/deployments/${deployment}/completions?api-version=${apiVersion}`;
return this.post(url, clone);
}

public createChatCompletion(
request: CreateChatCompletionRequest
): Promise<OpenAIClientResponse<CreateChatCompletionResponse>> {
const clone = Object.assign({}, request) as OpenAICreateChatCompletionRequest;
const deployment = this.removeModel(clone);
const deployment = request.model;
const endpoint = (this.options as AzureOpenAIClientOptions).endpoint;
const apiVersion = (this.options as AzureOpenAIClientOptions).apiVersion ?? '2023-03-15-preview';
const url = `${endpoint}/openai/deployments/${deployment}/chat/completions?api-version=${apiVersion}`;
Expand All @@ -74,7 +62,7 @@ export class AzureOpenAIClient extends OpenAIClient {

public createEmbedding(request: CreateEmbeddingRequest): Promise<OpenAIClientResponse<CreateEmbeddingResponse>> {
const clone = Object.assign({}, request) as OpenAICreateEmbeddingRequest;
const deployment = this.removeModel(clone);
const deployment = request.model;
const endpoint = (this.options as AzureOpenAIClientOptions).endpoint;
const apiVersion = (this.options as AzureOpenAIClientOptions).apiVersion ?? '2022-12-01';
const url = `${endpoint}/openai/deployments/${deployment}/embeddings?api-version=${apiVersion}`;
Expand All @@ -91,16 +79,8 @@ export class AzureOpenAIClient extends OpenAIClient {

protected addRequestHeaders(headers: Record<string, string>, options: OpenAIClientOptions): void {
headers[options.headerKey ?? 'api-key'] = options.apiKey;
}

private removeModel(request: { model?: string }): string {
const model = request.model;
delete request.model;

if (model) {
return model;
} else {
return '';
if (options.ocpApimSubscriptionKey) {
headers['Ocp-Apim-Subscription-Key'] = options.ocpApimSubscriptionKey;
}
}
}
Loading

0 comments on commit 5372e09

Please sign in to comment.