UNPKG

node-nlp

Version:

Library for NLU (Natural Language Understanding) done in Node.js

444 lines (409 loc) 17.2 kB
/** * @module botbuilder */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ import { Activity, ActivityTypes, ConversationReference, InputHints, ResourceResponse } from 'botframework-schema'; import { BotAdapter } from './botAdapter'; import { shallowCopy } from './internal'; /** * Signature implemented by functions registered with `context.onSendActivity()`. * * ```TypeScript * type SendActivitiesHandler = (context: TurnContext, activities: Partial<Activity>[], next: () => Promise<ResourceResponse[]>) => Promise<ResourceResponse[]>; * ``` */ export type SendActivitiesHandler = ( context: TurnContext, activities: Partial<Activity>[], next: () => Promise<ResourceResponse[]> ) => Promise<ResourceResponse[]>; /** * Signature implemented by functions registered with `context.onUpdateActivity()`. * * ```TypeScript * type UpdateActivityHandler = (context: TurnContext, activity: Partial<Activity>, next: () => Promise<void>) => Promise<void>; * ``` */ export type UpdateActivityHandler = (context: TurnContext, activity: Partial<Activity>, next: () => Promise<void>) => Promise<void>; /** * Signature implemented by functions registered with `context.onDeleteActivity()`. * * ```TypeScript * type DeleteActivityHandler = (context: TurnContext, reference: Partial<ConversationReference>, next: () => Promise<void>) => Promise<void>; * ``` */ export type DeleteActivityHandler = ( context: TurnContext, reference: Partial<ConversationReference>, next: () => Promise<void> ) => Promise<void>; // tslint:disable-next-line:no-empty-interface export interface TurnContext {} /** * Context object containing information cached for a single turn of conversation with a user. * * @remarks * This will typically be created by the adapter you're using and then passed to middleware and * your bots logic. */ export class TurnContext { private _adapter: BotAdapter | undefined; private _activity: Activity | undefined; private _respondedRef: { responded: boolean } = { responded: false }; private _turnState: Map<any, any> = new Map<any, any>(); private _onSendActivities: SendActivitiesHandler[] = []; private _onUpdateActivity: UpdateActivityHandler[] = []; private _onDeleteActivity: DeleteActivityHandler[] = []; /** * Creates a new TurnContext instance. * @param adapterOrContext Adapter that constructed the context or a context object to clone. * @param request Request being processed. */ constructor(adapterOrContext: BotAdapter, request: Partial<Activity>); constructor(adapterOrContext: TurnContext); constructor(adapterOrContext: BotAdapter|TurnContext, request?: Partial<Activity>) { if (adapterOrContext instanceof TurnContext) { adapterOrContext.copyTo(this); } else { this._adapter = adapterOrContext; this._activity = request as Activity; } } /** * Returns the conversation reference for an activity. * * @remarks * This can be saved as a plain old JSON object and then later used to message the user * proactively. * * ```JavaScript * const reference = TurnContext.getConversationReference(context.request); * ``` * @param activity The activity to copy the conversation reference from */ public static getConversationReference(activity: Partial<Activity>): Partial<ConversationReference> { return { activityId: activity.id, user: shallowCopy(activity.from), bot: shallowCopy(activity.recipient), conversation: shallowCopy(activity.conversation), channelId: activity.channelId, serviceUrl: activity.serviceUrl }; } /** * Updates an activity with the delivery information from a conversation reference. * * @remarks * Calling this after [getConversationReference()](#getconversationreference) on an incoming * activity will properly address the reply to a received activity. * * ```JavaScript * // Send a typing indicator without going through a middleware listeners. * const reference = TurnContext.getConversationReference(context.activity); * const activity = TurnContext.applyConversationReference({ type: 'typing' }, reference); * await context.adapter.sendActivities([activity]); * ``` * @param activity Activity to copy delivery information to. * @param reference Conversation reference containing delivery information. * @param isIncoming (Optional) flag indicating whether the activity is an incoming or outgoing activity. Defaults to `false` indicating the activity is outgoing. */ public static applyConversationReference( activity: Partial<Activity>, reference: Partial<ConversationReference>, isIncoming: boolean = false ): Partial<Activity> { activity.channelId = reference.channelId; activity.serviceUrl = reference.serviceUrl; activity.conversation = reference.conversation; if (isIncoming) { activity.from = reference.user; activity.recipient = reference.bot; if (reference.activityId) { activity.id = reference.activityId; } } else { activity.from = reference.bot; activity.recipient = reference.user; if (reference.activityId) { activity.replyToId = reference.activityId; } } return activity; } /** * Sends a single activity or message to the user. * * @remarks * This ultimately calls [sendActivities()](#sendactivites) and is provided as a convenience to * make formating and sending individual activities easier. * * ```JavaScript * await context.sendActivity(`Hello World`); * ``` * @param activityOrText Activity or text of a message to send the user. * @param speak (Optional) SSML that should be spoken to the user for the message. * @param inputHint (Optional) `InputHint` for the message sent to the user. Defaults to `acceptingInput`. */ public sendActivity(activityOrText: string|Partial<Activity>, speak?: string, inputHint?: string): Promise<ResourceResponse|undefined> { let a: Partial<Activity>; if (typeof activityOrText === 'string') { a = { text: activityOrText, inputHint: inputHint || InputHints.AcceptingInput }; if (speak) { a.speak = speak; } } else { a = activityOrText; } return this.sendActivities([a]).then( (responses: ResourceResponse[]) => responses && responses.length > 0 ? responses[0] : undefined ); } /** * Sends a set of activities to the user. An array of responses from the server will be returned. * * @remarks * Prior to delivery, the activities will be updated with information from the `ConversationReference` * for the contexts [activity](#activity) and if any activities `type` field hasn't been set it will be * set to a type of `message`. The array of activities will then be routed through any [onSendActivities()](#onsendactivities) * handlers before being passed to `adapter.sendActivities()`. * * ```JavaScript * await context.sendActivities([ * { type: 'typing' }, * { type: 'delay', value: 2000 }, * { type: 'message', text: 'Hello... How are you?' } * ]); * ``` * @param activities One or more activities to send to the user. */ public sendActivities(activities: Partial<Activity>[]): Promise<ResourceResponse[]> { let sentNonTraceActivity: boolean = false; const ref: Partial<ConversationReference> = TurnContext.getConversationReference(this.activity); const output: Partial<Activity>[] = activities.map((a: Partial<Activity>) => { const o: Partial<Activity> = TurnContext.applyConversationReference({...a}, ref); if (!o.type) { o.type = ActivityTypes.Message; } if (o.type !== ActivityTypes.Trace) { sentNonTraceActivity = true; } return o; }); return this.emit(this._onSendActivities, output, () => { return this.adapter.sendActivities(this, output) .then((responses: ResourceResponse[]) => { // Set responded flag if (sentNonTraceActivity) { this.responded = true; } return responses; }); }); } /** * Replaces an existing activity. * * @remarks * The activity will be routed through any registered [onUpdateActivity](#onupdateactivity) handlers * before being passed to `adapter.updateActivity()`. * * ```JavaScript * const matched = /approve (.*)/i.exec(context.activity.text); * if (matched) { * const update = await approveExpenseReport(matched[1]); * await context.updateActivity(update); * } * ``` * @param activity New replacement activity. The activity should already have it's ID information populated. */ public updateActivity(activity: Partial<Activity>): Promise<void> { return this.emit(this._onUpdateActivity, activity, () => this.adapter.updateActivity(this, activity)); } /** * Deletes an existing activity. * * @remarks * The `ConversationReference` for the activity being deleted will be routed through any registered * [onDeleteActivity](#ondeleteactivity) handlers before being passed to `adapter.deleteActivity()`. * * ```JavaScript * const matched = /approve (.*)/i.exec(context.activity.text); * if (matched) { * const savedId = await approveExpenseReport(matched[1]); * await context.deleteActivity(savedId); * } * ``` * @param idOrReference ID or conversation of the activity being deleted. If an ID is specified the conversation reference information from the current request will be used to delete the activity. */ public deleteActivity(idOrReference: string|Partial<ConversationReference>): Promise<void> { let reference: Partial<ConversationReference>; if (typeof idOrReference === 'string') { reference = TurnContext.getConversationReference(this.activity); reference.activityId = idOrReference; } else { reference = idOrReference; } return this.emit(this._onDeleteActivity, reference, () => this.adapter.deleteActivity(this, reference)); } /** * Registers a handler to be notified of, and potentially intercept, the sending of activities. * * @remarks * This example shows how to listen for and logs outgoing `message` activities. * * ```JavaScript * context.onSendActivities(await (ctx, activities, next) => { * // Deliver activities * await next(); * * // Log sent messages * activities.filter(a => a.type === 'message').forEach(a => logSend(a)); * }); * ``` * @param handler A function that will be called anytime [sendActivity()](#sendactivity) is called. The handler should call `next()` to continue sending of the activities. */ public onSendActivities(handler: SendActivitiesHandler): this { this._onSendActivities.push(handler); return this; } /** * Registers a handler to be notified of, and potentially intercept, an activity being updated. * * @remarks * This example shows how to listen for and logs updated activities. * * ```JavaScript * context.onUpdateActivities(await (ctx, activity, next) => { * // Replace activity * await next(); * * // Log update * logUpdate(activity); * }); * ``` * @param handler A function that will be called anytime [updateActivity()](#updateactivity) is called. The handler should call `next()` to continue sending of the replacement activity. */ public onUpdateActivity(handler: UpdateActivityHandler): this { this._onUpdateActivity.push(handler); return this; } /** * Registers a handler to be notified of, and potentially intercept, an activity being deleted. * * @remarks * This example shows how to listen for and logs deleted activities. * * ```JavaScript * context.onDeleteActivities(await (ctx, reference, next) => { * // Delete activity * await next(); * * // Log delete * logDelete(activity); * }); * ``` * @param handler A function that will be called anytime [deleteActivity()](#deleteactivity) is called. The handler should call `next()` to continue deletion of the activity. */ public onDeleteActivity(handler: DeleteActivityHandler): this { this._onDeleteActivity.push(handler); return this; } /** * Called when this TurnContext instance is passed into the constructor of a new TurnContext * instance. * * @remarks * Can be overridden in derived classes to add additional fields that should be cloned. * @param context The context object to copy private members to. Everything should be copied by reference. */ protected copyTo(context: TurnContext): void { // Copy private member to other instance. [ '_adapter', '_activity', '_respondedRef', '_services', '_onSendActivities', '_onUpdateActivity', '_onDeleteActivity' ].forEach((prop: string) => (context as any)[prop] = (this as any)[prop]); } /** * The adapter for this context. * * @remarks * This example shows how to send a `typing` activity directly using the adapter. This approach * bypasses any middleware which sometimes has its advantages. The calls to * `getConversationReference()` and `applyConversationReference()` are needed to ensure that the * outgoing activity is properly addressed: * * ```JavaScript * // Send a typing indicator without going through an middleware listeners. * const reference = TurnContext.getConversationReference(context.activity); * const activity = TurnContext.applyConversationReference({ type: 'typing' }, reference); * await context.adapter.sendActivities([activity]); * ``` */ public get adapter(): BotAdapter { return this._adapter as BotAdapter; } /** * The received activity. * * @remarks * This example shows how to get the users trimmed utterance from the activity: * * ```JavaScript * const utterance = (context.activity.text || '').trim(); * ``` */ public get activity(): Activity { return this._activity as Activity; } /** * If `true` at least one response has been sent for the current turn of conversation. * * @remarks * This is primarily useful for determining if a bot should run fallback routing logic: * * ```JavaScript * await routeActivity(context); * if (!context.responded) { * await context.sendActivity(`I'm sorry. I didn't understand.`); * } * ``` */ public get responded(): boolean { return this._respondedRef.responded; } public set responded(value: boolean) { if (!value) { throw new Error(`TurnContext: cannot set 'responded' to a value of 'false'.`); } this._respondedRef.responded = true; } /** * Map of services and other values cached for the lifetime of the turn. * * @remarks * Middleware, other components, and services will typically use this to cache information * that could be asked for by a bot multiple times during a turn. The bots logic is free to * use this to pass information between its own components. * * ```JavaScript * const cart = await loadUsersShoppingCart(context); * context.turnState.set('cart', cart); * ``` * * > [!TIP] * > For middleware and third party components, consider using a `Symbol()` for your cache key * > to avoid potential naming collisions with the bots caching and other components. */ public get turnState(): Map<any, any> { return this._turnState; } private emit<T>( handlers: ((context: TurnContext, arg: T, next: () => Promise<any>) => Promise<any>)[], arg: T, next: () => Promise<any> ): Promise<any> { const list: ((context: TurnContext, arg: T, next: () => Promise<any>) => Promise<any>)[] = handlers.slice(); const context: TurnContext = this; function emitNext(i: number): Promise<void> { try { if (i < list.length) { return Promise.resolve(list[i](context, arg, () => emitNext(i + 1))); } return Promise.resolve(next()); } catch (err) { return Promise.reject(err); } } return emitNext(0); } }