Skip to content

Commit

Permalink
Merge pull request microsoft#130 from southworkscom/node-luisactionbi…
Browse files Browse the repository at this point in the history
…nding-updates

[Node-LUISActionBinding] Add support for custom fulfill handling on `bindToBotDialog`
  • Loading branch information
willportnoy authored Jun 23, 2017
2 parents f2a744e + a7286a2 commit bc26d30
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 35 deletions.
30 changes: 17 additions & 13 deletions Node/blog-LUISActionBinding/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ The Console sample has a [custom handler](samples/console/app.js#L93-L122) that

In the [samples/bot/app.js](samples/bot/app.js) you can see how the Action Bindings are used within a bot application.

The key thing here is creating an `IntentDialog` with a `LuisRecognizer`, and bind the Actions array to the bot and dialog using the [`bindToBotDialog`](core/index.js#L181-L206) helper function. Here's a simplified version:
The key thing here is creating an `IntentDialog` with a `LuisRecognizer`, and bind the Actions array to the bot and dialog using the [`bindToBotDialog`](core/index.js#L181-L208) helper function. Here's a simplified version:

````JavaScript
var LuisModelUrl = process.env.LUIS_MODEL_URL;
Expand All @@ -330,28 +330,32 @@ var SampleActions = require('../all');
LuisActions.bindToBotDialog(bot, intentDialog, LuisModelUrl, SampleActions);
````

The [`bindToBotDialog`](core/index.js#L181-L206) function registers each Action Binding with the IntentDialog, using the action's `intentName`. Internally, it uses the dialog's [`matches()`](https://docs.botframework.com/en-us/node/builder/chat-reference/classes/_botbuilder_d_.intentdialog.html#matches) function.
The [`bindToBotDialog`](core/index.js#L181-L208) function registers each Action Binding with the IntentDialog, using the action's `intentName`. Internally, it uses the dialog's [`matches()`](https://docs.botframework.com/en-us/node/builder/chat-reference/classes/_botbuilder_d_.intentdialog.html#matches) function.

The function also registers with the bot a new BotBuilder library that contains a single dialog, named [`Evaluate`](core/index.js#L213).
Then, it uses the internal [`createBotAction`](core/index.js#L317) function to route incomming messages with matching intents to the [`Evaluate`](core/index.js#L213) dialog.
The function also registers with the bot a new BotBuilder library that contains a single dialog, named [`Evaluate`](core/index.js#L216).
Then, it uses the internal [`createBotAction`](core/index.js#L320) function to route incomming messages with matching intents to the [`Evaluate`](core/index.js#L216) dialog.

The `Evaluate` dialog extracts the entities from the [LUIS Result](https://docs.botframework.com/en-us/node/builder/chat-reference/interfaces/_botbuilder_d_.iintentrecognizerresult.html), maps them to parameters, validates the parameter values and, if possible, fulfills the action function, among other things.

When validation does not pass, [prompt messages](core/index.js#L280-L290) are presented to the user to provide each of the missing or invalid parameters and continues until validation passes. Then the [fulfill function is invoked](core/index.js#L300-L304), passing all validated parameters.
When validation does not pass, [prompt messages](core/index.js#L283-L293) are presented to the user to provide each of the missing or invalid parameters and continues until validation passes. Then the [fulfill function is invoked](core/index.js#L302-L307), passing all validated parameters.

The [`bindToBotDialog`](core/index.js#L181-L206) function accepts an optional `defaultReplyHandler` parameter, defined as a function that accepts a `session` object. This function is invoked when an intent is detected but not mapped to an action and can be used to reply with a custom message or trigger custom logic. An example is the [DefaultReplyHandler function](samples/bot/app.js#L11-L15).
The [`bindToBotDialog`](core/index.js#L181-L208) function accepts an optional set of arguments for customizing some of its behavior, as seen in the [bindToBotDialog call](samples/bot/app.js#L26-L30). The optional set of arguments are:

The [`bindToBotDialog`](core/index.js#L181-L206) function also accepts another optional `onContextCreationHandler` parameter used to re-hydrate context for a contextual action, as seen in [Scenario #3](#scenario-3--trigger-a-contextual-action-with-no-previous-context-ie-from-scratch).
- `defaultReply`: [Optional] Defined as a function that accepts a `session` object. This function is invoked when an intent is detected but not mapped to an action and can be used to reply with a custom message or trigger custom logic. An example is the [DefaultReplyHandler function](samples/bot/app.js#L32-L36).

- `fulfillReply`: [Optional] Defined as a function that accepts a `session` object and the fulfilled `actionModel`. It can be used to send to the user a customized message with the actionModel's result (e.g.: the result wrapped on a RichCard or AdaptiveCard) or do any further processing (e.g.: telemetry). An example is the [FulfillReplyHandler function](samples/bot/app.js#L38-L41).

- `onContextCreation`: [Optional] Function used to re-hydrate context for a contextual action, as seen in [Scenario #3](#scenario-3--trigger-a-contextual-action-with-no-previous-context-ie-from-scratch).
The parameter is defined as a funcion with four parameters:

- `action`: The Action Binding definition that triggered the context creation.
- `actionModel`: The action model, which contains the `parameters` object you'll want to complete.
- `next`: The method that must be called to continue the execution of the Action Binding. This is required to support asynchronous calls.
- `session`: The BotBuilder session object. Can be used to retrieve the user's address id or even send messages.
- `action`: The Action Binding definition that triggered the context creation.
- `actionModel`: The action model, which contains the `parameters` object you'll want to complete.
- `next`: The method that must be called to continue the execution of the Action Binding. This is required to support asynchronous calls.
- `session`: The BotBuilder session object. Can be used to retrieve the user's address id or even send messages.

The Bot sample has a [custom handler](samples/bot/app.js#L35-L67) that re-hydrates the FindHotels action context with default values.
The Bot sample has a [custom handler](samples/bot/app.js#L43-L75) that re-hydrates the FindHotels action context with default values.

> NOTE: When defining the `onContextCreationHandler` function, remember to call `next()` to continue executing the action binding's logic.
> NOTE: When defining the `onContextCreationHandler` function, remember to call `next()` to continue executing the action binding's logic.

#### Using LUIS Action Bindings within a Web Application

Expand Down
19 changes: 13 additions & 6 deletions Node/blog-LUISActionBinding/core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,25 @@ export function evaluate(
userInput?: string,
onContextCreationHandler?: onContextCreationHandler): PromiseLike<IActionModel>;

declare type onContextCreationHandler = (action: IAction, actionModel: IActionModel, next: () => void) => void

export function bindToBotDialog(
bot: UniversalBot,
intentDialog: IntentDialog,
modelUrl: string,
actions: Array<IAction>,
defaultReplyHandler?: replyHandler,
onContextCreationHandler?: onBotContextCreationHandler
options: IBindToDialogOptions
)

declare type onContextCreationHandler = (action: IAction, actionModel: IActionModel, next: () => void) => void
declare type onBotContextCreationHandler = (action: IAction, actionModel: IActionModel, next: () => void, session: Session) => void
declare type replyHandler = (session: Session) => void;
declare type onDialogContextCreationHandler = (action: IAction, actionModel: IActionModel, next: () => void, session: Session) => void
declare type replyHandler = (session: Session) => void
declare type fulfillHandler = (session: Session, actionModel: IActionModel) => void

export interface IBindToDialogOptions {
defaultReply: replyHandler,
fulfillReply: fulfillHandler,
onContextCreation: onDialogContextCreationHandler
}

// TYPES
export interface IAction {
Expand Down Expand Up @@ -62,4 +69,4 @@ export interface IActionModel {
export interface IParameterError {
parameterName: string;
message: string;
}
}
17 changes: 10 additions & 7 deletions Node/blog-LUISActionBinding/core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ function evaluate(modelUrl, actions, currentActionModel, userInput, onContextCre
/*
* Bot Stuff
*/
function bindToBotDialog(bot, intentDialog, modelUrl, actions, defaultReplyHandler, onContextCreationHandler) {
function bindToBotDialog(bot, intentDialog, modelUrl, actions, options) {
if (!bot) {
throw new Error('bot is required');
}
Expand All @@ -193,21 +193,24 @@ function bindToBotDialog(bot, intentDialog, modelUrl, actions, defaultReplyHandl
throw new Error('ModelUrl is required');
}

options = options || {};

// enable bot persistence (used for storing actionModel in privateConversationData)
bot.set('persistConversationData', true);

// register dialog for handling input evaluation
bot.library(createBotLibrary(modelUrl, actions, defaultReplyHandler, onContextCreationHandler));
bot.library(createBotLibrary(modelUrl, actions, options));

// Register each LuisActions with the intentDialog
_.forEach(actions, function (action) {
intentDialog.matches(action.intentName, createBotAction(action, modelUrl));
});
}

function createBotLibrary(modelUrl, actions, defaultReplyHandler, onContextCreationHandler) {
defaultReplyHandler = typeof defaultReplyHandler === 'function' ? defaultReplyHandler : function (session) { session.endDialog('Sorry, I couldn\'t understart that.'); };
onContextCreationHandler = validateContextCreationHandler(onContextCreationHandler);
function createBotLibrary(modelUrl, actions, options) {
var defaultReplyHandler = typeof options.defaultReply === 'function' ? options.defaultReply : function (session) { session.endDialog('Sorry, I couldn\'t understart that.'); };
var fulfillReplyHandler = typeof options.fulfillReply === 'function' ? options.fulfillReply : function (session, actionModel) { session.endDialog(actionModel.result.toString()); };
var onContextCreationHandler = validateContextCreationHandler(options.onContextCreation);

var lib = new builder.Library('LuisActions');
lib.dialog('Evaluate', new builder.SimpleDialog(function (session, args) {
Expand Down Expand Up @@ -294,13 +297,13 @@ function createBotLibrary(modelUrl, actions, defaultReplyHandler, onContextCreat
var prompt = actionModel.contextSwitchPrompt;
session.privateConversationData['luisaction.model'] = actionModel;
builder.Prompts.confirm(session, prompt, { listStyle: builder.ListStyle.button });

break;

case Status.Fulfilled:
// Action fulfilled
// TODO: Allow external handler
delete session.privateConversationData['luisaction.model'];
session.endDialog(actionModel.result.toString());
fulfillReplyHandler(session, actionModel);
break;

}
Expand Down
26 changes: 17 additions & 9 deletions Node/blog-LUISActionBinding/samples/bot/app.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
require('dotenv-extended').load({
path: '../.env'
});
require('dotenv-extended').load({ path: '../.env' });

var builder = require('botbuilder');
var restify = require('restify');

var LuisActions = require('../../core');
var SampleActions = require('../all');
var LuisModelUrl = process.env.LUIS_MODEL_URL;
var DefaultReplyHandler = function (session) {
session.endDialog(
'Sorry, I did not understand "%s". Use sentences like "What is the time in Miami?", "Search for 5 stars hotels in Barcelona", "Tell me the weather in Buenos Aires", "Location of SFO airport")',
session.message.text);
};

var server = restify.createServer();
server.listen(process.env.port || process.env.PORT || 3978, function () {
Expand All @@ -30,7 +23,22 @@ var recognizer = new builder.LuisRecognizer(LuisModelUrl);
var intentDialog = bot.dialog('/', new builder.IntentDialog({ recognizers: [recognizer] })
.onDefault(DefaultReplyHandler));

LuisActions.bindToBotDialog(bot, intentDialog, LuisModelUrl, SampleActions, DefaultReplyHandler, onContextCreationHandler);
LuisActions.bindToBotDialog(bot, intentDialog, LuisModelUrl, SampleActions, {
defaultReply: DefaultReplyHandler,
fulfillReply: FulfillReplyHandler,
onContextCreation: onContextCreationHandler
});

function DefaultReplyHandler(session) {
session.endDialog(
'Sorry, I did not understand "%s". Use sentences like "What is the time in Miami?", "Search for 5 stars hotels in Barcelona", "Tell me the weather in Buenos Aires", "Location of SFO airport")',
session.message.text);
}

function FulfillReplyHandler(session, actionModel) {
console.log('Action Binding "' + actionModel.intentName + '" completed:', actionModel);
session.endDialog(actionModel.result.toString());
}

function onContextCreationHandler(action, actionModel, next, session) {

Expand Down

0 comments on commit bc26d30

Please sign in to comment.