@microsoft/teams-ai
Version:
SDK focused on building AI based applications for Microsoft Teams.
378 lines • 16.8 kB
JavaScript
;
/**
* @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.AI = void 0;
const actions = __importStar(require("./actions"));
const moderators_1 = require("./moderators");
/**
* AI System.
* @remarks
* The AI system is responsible for generating plans, moderating input and output, and
* generating prompts. It can be used free standing or routed to by the Application object.
* @template TState Optional. Type of the turn state.
*/
class AI {
_actions = new Map();
_options;
/**
* A text string that can be returned from an action to stop the AI system from continuing
* to execute the current plan.
* @remarks
* This command is incompatible and should not be used with `tools` augmentation
*/
static StopCommandName = actions.StopCommandName;
/**
* An action that will be called anytime an unknown action is predicted by the planner.
* @remarks
* The default behavior is to simply log an error to the console. The plan is allowed to
* continue execution by default.
*/
static UnknownActionName = '___UnknownAction___';
/**
* An action that will be called anytime an input is flagged by the moderator.
* @remarks
* The default behavior is to simply log an error to the console. Override to send a custom
* message to the user.
*/
static FlaggedInputActionName = '___FlaggedInput___';
/**
* An action that will be called anytime an output is flagged by the moderator.
* @remarks
* The default behavior is to simply log an error to the console. Override to send a custom
* message to the user.
*/
static FlaggedOutputActionName = '___FlaggedOutput___';
/**
* An action that will be called anytime the planner encounters an HTTP response with
* status code >= `400`.
*/
static HttpErrorActionName = '___HttpError___';
/**
* The task either executed too many steps or timed out.
*/
static TooManyStepsActionName = '___TooManySteps___';
/**
* An action that will be called after the plan has been predicted by the planner and it has
* passed moderation.
* @remarks
* Overriding this action lets you customize the decision to execute a plan separately from the
* moderator. The default behavior is to proceed with the plans execution only with a plan
* contains one or more commands. Returning false from this action can be used to prevent the plan
* from being executed.
*/
static PlanReadyActionName = '___PlanReady___';
/**
* An action that is called to DO an action.
* @remarks
* The action system is used to do other actions. Overriding this action lets you customize the
* execution of an individual action. You can use it to log actions being used or to prevent
* certain actions from being executed based on policy.
*
* The default behavior is to simply execute the action handler passed in so you will need to
* perform that logic yourself should you override this action.
*/
static DoCommandActionName = '___DO___';
/**
* An action that is called to SAY something.
* @remarks
* Overriding this action lets you customize the execution of the SAY command. You can use it
* to log the output being generated or to add support for sending certain types of output as
* message attachments.
*
* The default behavior attempts to look for an Adaptive Card in the output and if found sends
* it as an attachment. If no Adaptive Card is found then the output is sent as a plain text
* message.
*
* If you override this action and want to automatically send Adaptive Cards as attachments you
* will need to handle that yourself.
*/
static SayCommandActionName = '___SAY___';
/**
* Creates a new AI system.
* @param {ConfiguredAIOptions} options The options used to configure the AI system.
*/
constructor(options) {
this._options = Object.assign({
max_steps: 25,
max_time: 300000,
allow_looping: true,
enable_feedback_loop: false
}, options);
// Create moderator if needed
if (!this._options.moderator) {
this._options.moderator = new moderators_1.DefaultModerator();
}
this.defaultAction(AI.UnknownActionName, actions.unknown());
this.defaultAction(AI.FlaggedInputActionName, actions.flaggedInput());
this.defaultAction(AI.FlaggedOutputActionName, actions.flaggedOutput());
this.defaultAction(AI.HttpErrorActionName, actions.httpError());
this.defaultAction(AI.PlanReadyActionName, actions.planReady());
this.defaultAction(AI.DoCommandActionName, actions.doCommand());
this.defaultAction(AI.TooManyStepsActionName, actions.tooManySteps());
this.defaultAction(AI.SayCommandActionName, actions.sayCommand(this._options.enable_feedback_loop, this._options.feedback_loop_type || 'default'));
}
/**
* Returns the moderator being used by the AI system.
* @remarks
* The default moderator simply allows all messages and plans through without intercepting them.
* @returns {Moderator} The AI's moderator
*/
get moderator() {
return this._options.moderator;
}
/**
* @returns {Planner<TState>} Returns the planner being used by the AI system.
*/
get planner() {
return this._options.planner;
}
/**
* @returns {boolean} Returns the feedback loop flag.
*/
get enableFeedbackLoop() {
return this._options.enable_feedback_loop;
}
/**
* @returns {boolean} Returns the feedback loop type.
*/
get feedbackLoopType() {
return this._options.feedback_loop_type;
}
/**
* Registers a handler for a named action.
* @remarks
* The AI systems planner returns plans that are made up of a series of commands or actions
* that should be performed. Registering a handler lets you provide code that should be run in
* response to one of the predicted actions.
*
* Plans support a DO command which specifies the name of an action to call and an optional
* set of entities that should be passed to the action. The internal plan executor will call
* the registered handler for the action passing in the current context, state, and entities.
*
* Additionally, the AI system itself uses actions to handle things like unknown actions,
* flagged input, and flagged output. You can override these actions by registering your own
* handler for them. The names of the built-in actions are available as static properties on
* the AI class.
* @template TParameters Optional. The type of parameters that the action handler expects.
* @param {string | string[]} name Unique name of the action.
* @param {actions.ActionHandler} handler The code to execute when the action's name is triggered.
* @returns {this} The AI system instance for chaining purposes.
*/
action(name, handler) {
(Array.isArray(name) ? name : [name]).forEach((n) => {
if (!this._actions.has(n)) {
this._actions.set(n, { handler, allowOverrides: false });
}
else {
const entry = this._actions.get(n);
if (entry.allowOverrides) {
entry.handler = handler;
entry.allowOverrides = false; // Only override once
}
else {
throw new Error(`The AI.action() method was called with a previously registered action named "${n}".`);
}
}
});
return this;
}
/**
* Registers the default handler for a named action.
* @remarks
* @param {string | string[]} name - Unique name of the action.
* @template TParameters - Optional. The type of parameters that the action handler expects.
* @param {actions.ActionHandler<TState, TParameters>} handler - The code to execute when the action's name is triggered.
* Default handlers can be replaced by calling the action() method with the same name.
* @returns {this} The AI system instance for chaining purposes.
*/
defaultAction(name, handler) {
(Array.isArray(name) ? name : [name]).forEach((n) => {
this._actions.set(n, { handler, allowOverrides: true });
});
return this;
}
/**
* Manually executes a named action.
* @template TParameters Optional. Type of entities expected to be passed to the action.
* @param {TurnContext} context Current turn context.
* @param {TState} state Current turn state.
* @param {string} action Name of the action to execute.
* @param {TParameters} parameters Optional. Entities to pass to the action.
* @returns {Promise<string>} The result of the action.
*/
async doAction(context, state, action, parameters) {
if (!this._actions.has(action)) {
throw new Error(`Can't find an action named '${action}'.`);
}
const handler = this._actions.get(action).handler;
return await handler(context, state, parameters, action);
}
/**
* Checks to see if the AI system has a handler for a given action.
* @param {string} action Name of the action to check.
* @returns {boolean} True if the AI system has a handler for the given action.
*/
hasAction(action) {
return this._actions.has(action);
}
/**
* Calls the configured planner to generate a plan and executes the plan that is returned.
* @remarks
* The moderator is called to review the input and output of the plan. If the moderator flags
* the input or output then the appropriate action is called. If the moderator allows the input
* and output then the plan is executed.
* @param {TurnContext} context Current turn context.
* @param {TState} state Current turn state.
* @param {number} start_time Optional. Time the AI system started running
* @param {number} step_count Optional. Number of steps that have been executed.
* @returns {Promise<boolean>} True if the plan was completely executed, otherwise false.
*/
async run(context, state, start_time, step_count) {
try {
// Initialize start time and action count
const { max_steps, max_time } = this._options;
if (start_time === undefined) {
start_time = Date.now();
}
if (step_count === undefined) {
step_count = 0;
}
// Review input on first loop
let plan = step_count == 0 ? await this._options.moderator.reviewInput(context, state) : undefined;
// Generate plan if moderator did not return one as flag for input.
if (!plan) {
if (step_count == 0) {
plan = await this._options.planner.beginTask(context, state, this);
}
else {
plan = await this._options.planner.continueTask(context, state, this);
}
// Review the plan's output
plan = await this._options.moderator.reviewOutput(context, state, plan);
}
// Process generated plan
let completed = false;
const response = await this._actions
.get(AI.PlanReadyActionName)
.handler(context, state, plan, AI.PlanReadyActionName);
if (response == AI.StopCommandName) {
return false;
}
// Run predicted commands
// - If the plan ends on a SAY command then the plan is considered complete, otherwise we'll loop
completed = true;
let should_loop = false;
for (let i = 0; i < plan.commands.length; i++) {
// Check for timeout
if (Date.now() - start_time > max_time || ++step_count > max_steps) {
completed = false;
const parameters = {
max_steps,
max_time,
start_time: start_time,
step_count: step_count
};
await this._actions
.get(AI.TooManyStepsActionName)
.handler(context, state, parameters, AI.TooManyStepsActionName);
break;
}
let output;
const cmd = plan.commands[i];
switch (cmd.type) {
case 'DO': {
const { action, actionId } = cmd;
if (this._actions.has(action)) {
// Call action handler
const handler = this._actions.get(action).handler;
output = await this._actions
.get(AI.DoCommandActionName)
.handler(context, state, { handler, ...cmd }, action);
// Set output for action call
if (actionId) {
should_loop = true;
state.temp.actionOutputs[actionId] = output ?? '';
}
else {
should_loop = output.length > 0;
state.temp.actionOutputs[action] = output;
}
}
else {
// Redirect to UnknownAction handler
output = await this._actions
.get(AI.UnknownActionName)
.handler(context, state, plan, action);
}
break;
}
case 'SAY':
should_loop = false;
output = await this._actions
.get(AI.SayCommandActionName)
.handler(context, state, cmd, AI.SayCommandActionName);
break;
default:
throw new Error(`AI.run(): unknown command of '${cmd.type}' predicted.`);
}
// Check for stop command
if (output == AI.StopCommandName) {
completed = false;
break;
}
// Copy the actions output to the input
state.temp.lastOutput = output;
state.temp.inputFiles = [];
if (cmd.type === 'DO' && cmd.actionId) {
state.deleteValue('temp.input');
}
else {
state.temp.input = output;
}
}
// Check for looping
if (completed && should_loop && this._options.allow_looping) {
return await this.run(context, state, start_time, step_count);
}
return completed;
}
catch (err) {
const onHttpError = this._actions.get(AI.HttpErrorActionName);
if (onHttpError) {
await onHttpError.handler(context, state, err, AI.HttpErrorActionName);
}
return false;
}
}
}
exports.AI = AI;
//# sourceMappingURL=AI.js.map