@microsoft/teams-ai
Version:
SDK focused on building AI based applications for Microsoft Teams.
495 lines • 22 kB
JavaScript
"use strict";
/**
* @module teams-ai
*/
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OpenAIModel = void 0;
const events_1 = __importDefault(require("events"));
const openai_1 = require("openai");
const internals_1 = require("../internals");
/**
* A `PromptCompletionModel` for calling OpenAI and Azure OpenAI hosted models.
* @remarks
* The model has been updated to support calling OpenAI's new o1 family of models. That currently
* comes with a few constraints. These constraints are mostly handled for you but are worth noting:
* - The o1 models introduce a new `max_completion_tokens` parameter and they've deprecated the
* `max_tokens` parameter. The model will automatically convert the incoming `max_tokens` parameter
* to `max_completion_tokens` for you. But you should be aware that o1 has hidden token usage and costs
* that aren't constrained by the `max_completion_tokens` parameter. This means that you may see an
* increase in token usage and costs when using the o1 models.
* - The o1 models do not currently support the sending of system messages which just means that the
* `useSystemMessages` parameter is ignored when calling the o1 models.
* - The o1 models do not currently support setting the `temperature`, `top_p`, and `presence_penalty`
* parameters so they will be ignored.
* - The o1 models do not currently support the use of tools so you will need to use the "monologue"
* augmentation to call actions.
*/
class OpenAIModel {
_events = new events_1.default();
_client;
_useAzure;
/**
* Options the client was configured with.
*/
options;
/**
* Creates a new `OpenAIModel` instance.
* @param {OpenAIModelOptions} options - Options for configuring the model client.
*/
constructor(options) {
// Handle deprecated options
if (options.maxRetries == undefined && options.retryPolicy != undefined) {
console.warn(`OpenAIModel: The 'retryPolicy' option is deprecated. Use 'maxRetries' instead.`);
options.maxRetries = options.retryPolicy.length;
}
if (options.clientOptions == undefined && options.requestConfig != undefined) {
console.warn(`OpenAIModel: The 'requestConfig' option is deprecated. Use 'clientOptions' instead.`);
options.clientOptions = {
timeout: options.requestConfig.timeout,
httpAgent: options.requestConfig.httpsAgent ?? options.requestConfig.httpAgent,
defaultHeaders: options.requestConfig.headers
};
}
// Check for azure config
if (options.azureApiKey ||
options.azureADTokenProvider) {
// Initialize options
this.options = Object.assign({
completion_type: 'chat',
azureApiVersion: '2023-05-15',
useSystemMessages: false
}, options);
// Cleanup and validate endpoint
let endpoint = this.options.azureEndpoint.trim();
if (endpoint.endsWith('/')) {
endpoint = endpoint.substring(0, endpoint.length - 1);
}
if (!endpoint.toLowerCase().startsWith('https://')) {
throw new Error(`Model created with an invalid endpoint of '${endpoint}'. The endpoint must be a valid HTTPS url.`);
}
this.options.azureEndpoint = endpoint;
// Create client
// - NOTE: we're not passing in a deployment as that hardcodes the deployment used.
this._useAzure = true;
this._client = new openai_1.AzureOpenAI(Object.assign({}, this.options.clientOptions, {
apiKey: this.options.azureApiKey ?? null,
endpoint: this.options.azureEndpoint,
apiVersion: this.options.azureApiVersion,
azureADTokenProvider: this.options.azureADTokenProvider
}));
}
else {
// Initialize options
this.options = Object.assign({
completion_type: 'chat',
useSystemMessages: false
}, options);
// Create client
this._useAzure = false;
this._client = new openai_1.OpenAI(Object.assign({}, this.options.clientOptions, {
apiKey: this.options.apiKey,
baseURL: this.options.endpoint,
organization: this.options.organization ?? null,
project: this.options.project ?? null
}));
}
}
/**
* Events emitted by the model.
* @returns {PromptCompletionModelEmitter} The events emitted by the model.
*/
get events() {
return this._events;
}
/**
* Completes a prompt using OpenAI or Azure OpenAI.
* @param {TurnContext} context - Current turn context.
* @param {Memory} memory - An interface for accessing state values.
* @param {PromptFunctions} functions - Functions to use when rendering the prompt.
* @param {Tokenizer} tokenizer - Tokenizer to use when rendering the prompt.
* @param {PromptTemplate} template - Prompt template to complete.
* @returns {Promise<PromptResponse<string>>} A `PromptResponse` with the status and message.
*/
async completePrompt(context, memory, functions, tokenizer, template) {
const startTime = Date.now();
const max_input_tokens = template.config.completion.max_input_tokens;
const model = template.config.completion.model ??
(this._useAzure
? this.options.azureDefaultDeployment
: this.options.defaultModel);
// Check for legacy completion type
if (template.config.completion.completion_type == 'text') {
throw new Error(`The completion_type 'text' is no longer supported. Only 'chat' based models are supported.`);
}
// Signal start of completion
const streaming = this.options.stream;
this._events.emit('beforeCompletion', context, memory, functions, tokenizer, template, !!streaming);
// Render prompt
const result = await template.prompt.renderAsMessages(context, memory, functions, tokenizer, max_input_tokens);
if (result.tooLong) {
return this.returnTooLong(max_input_tokens, result.length);
}
// Check for use of system messages
// - 'user' messages tend to be followed better by the model then 'system' messages.
const isO1Model = model.startsWith('o1-');
const useSystemMessages = !isO1Model && this.options.useSystemMessages;
if (!useSystemMessages && result.output.length > 0 && result.output[0].role == 'system') {
result.output[0].role = 'user';
}
// Log the generated prompt
if (this.options.logRequests) {
console.log(internals_1.Colorize.title('CHAT PROMPT:'));
console.log(internals_1.Colorize.output(result.output));
}
// Format messages to ChatCompletionMessageParam[]
const updatedMessages = this.convertMessages(result.output);
// Get input message
// - we're doing this here because the input message can be complex and include images.
const input = this.getInputMessage(result.output);
try {
// Get the chat completion parameters
const params = this.getChatCompletionParams(model, updatedMessages, template);
if (isO1Model) {
if (params.max_tokens) {
params.max_completion_tokens = params.max_tokens;
delete params.max_tokens;
}
params.temperature = 1;
params.top_p = 1;
params.presence_penalty = 0;
}
// Check for tools augmentation
const isToolsAugmentation = template.config.augmentation && template.config.augmentation?.augmentation_type == 'tools';
// Call chat completion API
let message;
const completion = await this._client.chat.completions.create(params);
if (params.stream) {
// Log start of streaming
if (this.options.logRequests) {
console.log(internals_1.Colorize.title('STREAM STARTED:'));
}
// Enumerate the streams chunks
message = { role: 'assistant', content: '' };
for await (const chunk of completion) {
const delta = chunk.choices[0]?.delta || {};
if (delta.role) {
message.role = delta.role;
}
if (delta.content) {
message.content += delta.content;
}
// Handle tool calls
// - We don't know how many tool calls there will be so we need to add them one-by-one.
if (isToolsAugmentation && delta.tool_calls) {
// Create action calls array if it doesn't exist
if (!Array.isArray(message.action_calls)) {
message.action_calls = [];
}
// Add tool calls to action calls
for (const toolCall of delta.tool_calls) {
// Add empty tool call to message if new index
// - Note that a single tool call can span multiple chunks.
const index = toolCall.index;
if (index >= message.action_calls.length) {
message.action_calls.push({
id: '',
function: { name: '', arguments: '' },
type: ''
});
}
// Set ID if provided
if (toolCall.id) {
message.action_calls[index].id = toolCall.id;
}
// Set type if provided
if (toolCall.type) {
message.action_calls[index].type = toolCall.type;
}
// Append function name if provided
if (toolCall.function?.name) {
message.action_calls[index].function.name += toolCall.function.name;
}
// Append function arguments if provided
if (toolCall.function?.arguments) {
message.action_calls[index].function.arguments += toolCall.function.arguments;
}
}
}
// Signal chunk received
if (this.options.logRequests) {
console.log(internals_1.Colorize.value('CHUNK', delta));
}
this._events.emit('chunkReceived', context, memory, { delta: delta });
}
// Log stream completion
if (this.options.logRequests) {
console.log(internals_1.Colorize.title('STREAM COMPLETED:'));
console.log(internals_1.Colorize.value('duration', Date.now() - startTime, 'ms'));
}
}
else {
const responseMessage = completion.choices[0].message;
message = {
role: responseMessage.role,
content: responseMessage.content ?? ''
};
// Preserve message context if there is any
const messageWithContext = responseMessage;
if (messageWithContext.context) {
message.context = messageWithContext.context;
}
const actionCalls = [];
// Log tool calls to be added to message of type Message<string> as action_calls
if (isToolsAugmentation && responseMessage?.tool_calls) {
for (const toolCall of responseMessage.tool_calls) {
actionCalls.push({
id: toolCall.id,
function: {
name: toolCall.function.name,
arguments: toolCall.function.arguments
},
type: toolCall.type
});
}
}
if (actionCalls.length > 0) {
message.action_calls = actionCalls;
}
// Log the generated response
if (this.options.logRequests) {
console.log(internals_1.Colorize.title('CHAT RESPONSE:'));
console.log(internals_1.Colorize.value('duration', Date.now() - startTime, 'ms'));
console.log(internals_1.Colorize.output(message));
}
}
// Signal response received
const response = { status: 'success', input, message };
const streamer = memory.getValue('temp.streamer');
this._events.emit('responseReceived', context, memory, response, streamer);
// Let any pending events flush before returning
await new Promise((resolve) => setTimeout(resolve, 0));
return response;
}
catch (err) {
console.log(err);
return this.returnError(err, input);
}
}
/**
* Converts the messages to ChatCompletionMessageParam[].
* @param {Message<string>} messages - The messages from result.output.
* @returns {ChatCompletionMessageParam[]} - The converted messages.
*/
convertMessages(messages) {
const params = [];
// Iterate through the messages and check for action calls
for (const message of messages) {
let param = {
role: 'user',
content: ''
};
if (message.role === 'user') {
param.content = message.content ?? '';
}
else if (message.role === 'system') {
param = {
role: 'system',
content: message.content ?? ''
};
}
else if (message.role === 'assistant') {
param = {
role: 'assistant',
content: message.content ?? ''
};
const toolCallParams = [];
if (message.action_calls && message.action_calls.length > 0) {
for (const toolCall of message.action_calls) {
toolCallParams.push({
id: toolCall.id,
function: {
name: toolCall.function.name,
arguments: toolCall.function.arguments
},
type: toolCall.type
});
}
param.tool_calls = toolCallParams;
}
}
else if (message.role === 'tool') {
param = {
role: 'tool',
content: message.content ?? '',
tool_call_id: message.action_call_id ?? ''
};
}
else {
param = {
role: 'function',
content: message.content ?? '',
name: message.name ?? ''
};
}
params.push(param);
}
return params;
}
/**
* @private
* @template TRequest
* @param {Partial<TRequest>} target - The target TRequest.
* @param {any} src - The source object.
* @param {string[]} fields - List of fields to copy.
* @returns {TRequest} The TRequest
*/
copyOptionsToRequest(target, src, fields) {
for (const field of fields) {
if (src[field] !== undefined) {
target[field] = src[field];
}
}
return target;
}
/**
* @private
* @param {string} model - Model to use.
* @param {ChatCompletionMessageParam[]} messages - Messages to send.
* @param {PromptTemplate} template Prompt template being used.
* @returns {ChatCompletionCreateParams} Chat completion parameters.
*/
getChatCompletionParams(model, messages, template) {
let completion = template.config.completion;
// Validate Tools Augmentation
const isToolsAugmentation = template.config.augmentation && template.config.augmentation?.augmentation_type == 'tools';
if (isToolsAugmentation) {
const chatCompletionTools = isToolsAugmentation
? template.actions?.map((action) => {
const chatCompletionTool = {
type: 'function',
function: {
name: action.name,
description: action.description ?? '',
parameters: action.parameters ?? {}
}
};
return chatCompletionTool;
})
: [];
const parallelToolCalls = template.config.completion.parallel_tool_calls || undefined;
completion = {
...completion,
tool_choice: template.config.completion.tool_choice ?? 'auto',
...(chatCompletionTools && chatCompletionTools.length > 0 && { tools: chatCompletionTools }),
// Only include parallel_tool_calls if tools are enabled and the template has it set; otherwise, it will default to true without being added to the API call
...(!!parallelToolCalls && { parallel_tool_calls: parallelToolCalls })
};
}
const params = this.copyOptionsToRequest({
messages: messages
}, completion, [
'max_tokens',
'temperature',
'top_p',
'n',
'stream',
'logprobs',
'top_logprobs',
'stop',
'presence_penalty',
'frequency_penalty',
'logit_bias',
'user',
'functions',
'function_call',
'data_sources',
'response_format',
'seed',
'tool_choice',
'tools',
'parallel_tool_calls'
]);
if (this.options.responseFormat) {
params.response_format = this.options.responseFormat;
}
if (this.options.seed !== undefined) {
params.seed = this.options.seed;
}
if (this.options.stream) {
params.stream = true;
}
params.model = model;
// Remove tool params if not using tools
if (!Array.isArray(params.tools) || params.tools.length == 0) {
if (params.tool_choice) {
delete params.tool_choice;
}
}
return params;
}
getInputMessage(messages) {
const last = messages.length - 1;
if (last > 0 && messages[last].role !== 'assistant') {
// Handling for when there are multiple action output responses (e.g. user message instantiated multiple tool calls)
if (messages[last].role === 'tool') {
const toolsInput = [];
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].action_calls) {
break;
}
toolsInput.unshift(messages[i]);
}
return toolsInput;
}
return messages[last];
}
return undefined;
}
returnTooLong(max_input_tokens, length) {
return {
status: 'too_long',
input: undefined,
error: new Error(`The generated chat completion prompt had a length of ${length} tokens which exceeded the max_input_tokens of ${max_input_tokens}.`)
};
}
returnError(err, input) {
if (err instanceof openai_1.OpenAI.APIError) {
if (this.options.logRequests) {
console.log(internals_1.Colorize.title('ERROR:'));
console.log(internals_1.Colorize.output(err.message));
console.log(internals_1.Colorize.title('HEADERS:'));
console.log(internals_1.Colorize.output(err.headers));
}
if (err.status == 429) {
return {
status: 'rate_limited',
input,
error: new Error(`The chat completion API returned a rate limit error.`)
};
}
else {
return {
status: 'error',
input,
error: new Error(`The chat completion API returned an error status of ${err.status}: ${err.name}`)
};
}
}
else {
return {
status: 'error',
input,
error: new Error(`The chat completion API returned an error: ${err.toString()}`)
};
}
}
}
exports.OpenAIModel = OpenAIModel;
//# sourceMappingURL=OpenAIModel.js.map