UNPKG

@microsoft/teams-ai

Version:

SDK focused on building AI based applications for Microsoft Teams.

387 lines 16.3 kB
"use strict"; /** * @module teams-ai */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AssistantsPlanner = void 0; const openai_1 = __importStar(require("openai")); const AI_1 = require("../AI"); /** * @private */ const DEFAULT_POLLING_INTERVAL = 1000; /** * @private */ const DEFAULT_ASSISTANTS_STATE_VARIABLE = 'conversation.assistants_state'; /** * @private */ const SUBMIT_TOOL_OUTPUTS_VARIABLE = 'temp.submitToolOutputs'; /** * @private */ const SUBMIT_TOOL_OUTPUTS_MAP = 'temp.submitToolMap'; /** * A Planner that uses the OpenAI Assistants API to generate plans for the AI system. * @template TState Optional. Type of application state. * @remarks * This planner manages conversations through OpenAI's thread-based system, handling: * - Thread creation and management * - Message submission * - Tool/function calling * - Response processing */ class AssistantsPlanner { _options; _client; /** * Creates a new `AssistantsPlanner` instance. * @param {AssistantsPlannerOptions} options - Options for configuring the AssistantsPlanner. */ constructor(options) { this._options = { polling_interval: DEFAULT_POLLING_INTERVAL, assistants_state_variable: DEFAULT_ASSISTANTS_STATE_VARIABLE, apiVersion: '2024-02-15-preview', ...options }; this._client = AssistantsPlanner.createClient(options.apiKey, options.endpoint, this._options); } /** * Starts a new task. * @remarks * This method is called when the AI system is ready to start a new task. It delegates the * task handling to the `continueTask` method. The planner should * generate a plan that the AI system will execute. Returning an empty plan signals that * there is no work to be performed. * * The planner should take the users input from `state.temp.input`. * @param {TurnContext} context - Context for the current turn of conversation. * @param {TState} state - Application state for the current turn of conversation. * @param {AI<TState>} ai - The AI system that is generating the plan. * @returns {Promise<Plan>} The plan that was generated. */ async beginTask(context, state, ai) { const threadId = await this.ensureThreadCreated(state, context.activity.text); await this.blockOnInProgressRuns(threadId); return await this.submitUserInput(context, state, ai); } /** * Continues the current task. * @remarks * This method is called when the AI system is ready to continue the current task. It handles: * - Creating a new thread if one doesn't exist * - Submitting tool outputs if required * - Waiting for any in-progress runs to complete * - Submitting user input and creating a new run * The method generates a plan that the AI system will execute. Returning an empty plan signals * that the task is completed and there is no work to be performed. * * The output from the last plan step that was executed is passed to the planner via `state.temp.input`. * @param {TurnContext} context - Context for the current turn of conversation. * @param {TState} state - Application state for the current turn of conversation. * @param {AI<TState>} ai - The AI system that is generating the plan. * @returns {Promise<Plan>} The plan that was generated. */ async continueTask(context, state, ai) { return await this.submitActionResults(context, state, ai); } /** * Creates a new assistant using the OpenAI Assistants API. * @param {string} apiKey - OpenAI API key. * @param {OpenAI.Beta.AssistantCreateParams} request - Definition of the assistant to create. * @param {string} endpoint - Optional. The Azure OpenAI resource endpoint. Required when using Azure OpenAI. * @param {Record<string, string | undefined>} azureClientOptions - Optional. The Azure OpenAI client options. * @returns {Promise<OpenAI.Beta.Assistant>} The created assistant. * @throws {Error} If the assistant creation fails. */ static async createAssistant(apiKey, request, endpoint, azureClientOptions) { const client = AssistantsPlanner.createClient(apiKey, endpoint, azureClientOptions); return await client.beta.assistants.create(request); } /** * Ensures a thread exists for the current conversation. * @private * @param {TurnState} state - The application Turn State. * @param {string} input - The initial thread input. * @returns {Promise<string>} The thread ID. */ async ensureThreadCreated(state, input) { const assistantsState = this.ensureAssistantsState(state); if (assistantsState.thread_id == null) { const thread = await this._client.beta.threads.create(); assistantsState.thread_id = thread.id; this.updateAssistantsState(state, assistantsState); } return assistantsState.thread_id; } /** * Checks if a run has reached a terminal state. * @private * @param {OpenAI.Beta.Threads.Run} run - The run to check. * @returns {boolean} True if the run is in a completed state. */ isRunCompleted(run) { return ['completed', 'failed', 'cancelled', 'expired'].includes(run.status); } /** * @private * Waits for a run to complete. * @param {string} thread_id - The ID of the thread. * @param {string} run_id - The ID of the run. * @param {boolean} handleActions - Whether to handle actions. * @returns {Promise<OpenAI.Beta.Threads.Run>} The completed run. */ async waitForRun(thread_id, run_id, handleActions = false) { while (true) { await new Promise((resolve) => setTimeout(resolve, this._options.polling_interval)); const run = await this._client.beta.threads.runs.retrieve(thread_id, run_id); if ((run.status === 'requires_action' && handleActions) || this.isRunCompleted(run)) { return run; } } } /** * @private * Submits action results to the assistant. * @param {TurnContext} context - The turn context. * @param {TState} state - The turn state. * @param {AI<TState>} ai - The AI instance. * @returns {Promise<Plan>} A plan based on the action results. */ async submitActionResults(context, state, ai) { const assistantsState = this.ensureAssistantsState(state); const actionOutputs = state.temp.actionOutputs; const toolMap = state.getValue(SUBMIT_TOOL_OUTPUTS_MAP); const toolOutputs = []; for (const action in actionOutputs) { const output = actionOutputs[action]; const toolCallId = toolMap[action]; if (toolCallId !== undefined) { toolOutputs.push({ tool_call_id: toolCallId, output }); } } const run = await this._client.beta.threads.runs.submitToolOutputsAndPoll(assistantsState.thread_id, assistantsState.run_id, { tool_outputs: toolOutputs }); const results = await this.waitForRun(assistantsState.thread_id, run.id, true); switch (results.status) { case 'requires_action': { state.setValue(SUBMIT_TOOL_OUTPUTS_VARIABLE, true); return this.generatePlanFromTools(state, results.required_action.submit_tool_outputs.tool_calls); } case 'completed': { state.setValue(SUBMIT_TOOL_OUTPUTS_VARIABLE, false); return this.generatePlanFromMessages(assistantsState.thread_id, assistantsState.last_message_id); } case 'cancelled': { return { type: 'plan', commands: [] }; } case 'expired': { const expiredCommand = { type: 'DO', action: AI_1.AI.TooManyStepsActionName, parameters: {} }; return { type: 'plan', commands: [expiredCommand] }; } default: throw new Error(`Run failed ${results.status}. ErrorCode: ${results.last_error?.code}. ErrorMessage: ${results.last_error?.message}`); } } /** * @private * Submits user input to the assistant. * @param {TurnContext} context - The turn context. * @param {TState} state - The turn state. * @param {AI<TState>} ai - The AI instance. * @returns {Promise<Plan>} A plan based on the user input. */ async submitUserInput(context, state, ai) { const threadId = await this.ensureThreadCreated(state, context.activity.text); const message = await this._client.beta.threads.messages.create(threadId, { role: 'user', content: state.temp.input }); const run = await this._client.beta.threads.runs.createAndPoll(threadId, { assistant_id: this._options.assistant_id }); this.updateAssistantsState(state, { thread_id: threadId, run_id: run.id, last_message_id: message.id }); const results = await this.waitForRun(threadId, run.id, true); switch (results.status) { case 'requires_action': state.setValue(SUBMIT_TOOL_OUTPUTS_VARIABLE, true); return this.generatePlanFromTools(state, results.required_action.submit_tool_outputs.tool_calls); case 'completed': { state.setValue(SUBMIT_TOOL_OUTPUTS_VARIABLE, false); return this.generatePlanFromMessages(threadId, message.id); } case 'cancelled': { return { type: 'plan', commands: [] }; } case 'expired': { const expiredCommand = { type: 'DO', action: AI_1.AI.TooManyStepsActionName, parameters: {} }; return { type: 'plan', commands: [expiredCommand] }; } default: throw new Error(`Run failed ${results.status}. ErrorCode: ${results.last_error?.code}. ErrorMessage: ${results.last_error?.message}`); } } /** * @private * Generates a plan from tool calls. * @param {TState} state - The turn state. * @param {OpenAI.Beta.Threads.Runs.RequiredActionFunctionToolCall[]} toolCalls - The tool calls to generate the plan from. * @returns {Plan} A plan based on the tool calls. */ generatePlanFromTools(state, toolCalls) { const plan = { type: 'plan', commands: [] }; const toolMap = {}; toolCalls.forEach((toolCall) => { toolMap[toolCall.function.name] = toolCall.id; const doCommand = { type: 'DO', action: toolCall.function.name, parameters: JSON.parse(toolCall.function.arguments) }; plan.commands.push(doCommand); }); state.setValue(SUBMIT_TOOL_OUTPUTS_MAP, toolMap); return plan; } /** * @private * Generates a plan from messages. * @param {string} thread_id - The ID of the thread. * @param {string} last_message_id - The ID of the last message. * @returns {Promise<Plan>} A plan based on the messages. */ async generatePlanFromMessages(thread_id, last_message_id) { const messages = await this._client.beta.threads.messages.list(thread_id); const lastMessageIndex = messages.data.findIndex((message) => message.id === last_message_id); const newMessages = lastMessageIndex >= 0 ? messages.data.slice(0, lastMessageIndex) : []; const plan = { type: 'plan', commands: [] }; newMessages.forEach((message) => { message.content.forEach((content) => { if (content.type === 'text') { const sayCommand = { type: 'SAY', response: { role: 'assistant', content: content.text.value, context: { intent: '', citations: content.text.annotations?.map((annotation) => ({ title: '', url: '', filepath: '', content: annotation.text })) || [] } } }; plan.commands.push(sayCommand); } }); }); return plan; } /** * @private * Ensures that the assistants state exists in the turn state. * @param {TState} state - The turn state. * @returns {AssistantsState} The assistants state. */ ensureAssistantsState(state) { if (!state.hasValue(this._options.assistants_state_variable)) { state.setValue(this._options.assistants_state_variable, { thread_id: null, run_id: null, last_message_id: null }); } return state.getValue(this._options.assistants_state_variable); } /** * @private * Updates the assistants state in the turn state. * @param {TState} state - The turn state. * @param {AssistantsState} assistantsState - The new assistants state. */ updateAssistantsState(state, assistantsState) { state.setValue(this._options.assistants_state_variable, assistantsState); } /** * @private * Blocks until all in-progress runs are completed. * @param {string} thread_id - The ID of the thread. */ async blockOnInProgressRuns(thread_id) { while (true) { const runs = await this._client.beta.threads.runs.list(thread_id, { limit: 1 }); if (runs.data.length === 0) { return; // No runs, so we're done } const latestRun = runs.data[0]; if (this.isRunCompleted(latestRun)) { return; // Latest run is completed, so we're done } await this.waitForRun(thread_id, latestRun.id); } } /** * @private * @param {string} apiKey - The api key * @param {string} endpoint - The Azure OpenAI resource endpoint * @param {Record<string, string | undefined>} azureClientOptions - The Azure OpenAI client options. * @returns {OpenAI} the client */ static createClient(apiKey, endpoint, azureClientOptions) { if (endpoint) { // Azure OpenAI return new openai_1.AzureOpenAI({ endpoint, apiKey, ...azureClientOptions }); } else { // OpenAI return new openai_1.default({ apiKey }); } } } exports.AssistantsPlanner = AssistantsPlanner; //# sourceMappingURL=AssistantsPlanner.js.map