UNPKG

@microsoft/teams-ai

Version:

SDK focused on building AI based applications for Microsoft Teams.

495 lines 22 kB
"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