Skip to content

Commit

Permalink
Updated LKG build, unit tests, and package.json version.
Browse files Browse the repository at this point in the history
  • Loading branch information
Stevenic committed Apr 8, 2016
1 parent 43f8166 commit 08fa258
Show file tree
Hide file tree
Showing 15 changed files with 306 additions and 48 deletions.
32 changes: 4 additions & 28 deletions Node/examples/basics-validatedPrompt/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ var bot = new builder.TextBot();
bot.add('/', [
function (session) {
// call custom prompt
session.beginDialog('/meaningOfLifePrompt', { maxRetries: 3 });
session.beginDialog('/meaningOfLife', { prompt: "What's the meaning of life?" });
},
function (session, results) {
// Check their answer
Expand All @@ -25,34 +25,10 @@ bot.add('/', [
session.send("Sorry you couldn't figure it out. Everyone knows that the meaning of life is 42.");
}
}

]);

bot.add('/meaningOfLifePrompt', function (session, results) {
results = results || {};

// Validate response
var valid = false;
if (results.response) {
valid = (results.response == '42');
}

// Return results or prompt the user
if (valid || (results.resumed == builder.ResumeReason.canceled)) {
// The user either answered the question correctly or they canceled by saying "nevermind"
session.endDialog(results);
} else if (!session.dialogData.hasOwnProperty('maxRetries')) {
// First call to the pormpt so process args passed to the prompt
session.dialogData.maxRetries = results.maxRetries || 2;
builder.Prompts.text(session, "What's the meaning of life?");
} else if (session.dialogData.maxRetries > 0) {
// User guessed wrong but they have retries left
session.dialogData.maxRetries--;
builder.Prompts.text(session, "Sorry that's not it. Guess again. What's the meaning of life?");
} else {
// User to failed to guess in alloted number of tries
session.endDialog({ resumed: builder.ResumeReason.notCompleted });
}
});
bot.add('/meaningOfLife', builder.DialogAction.validatedPrompt(builder.PromptType.text, function (response) {
return response === '42';
}));

bot.listenStdin();
40 changes: 40 additions & 0 deletions Node/lib/Message.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
var session = require('./Session');
var Message = (function () {
function Message() {
}
Message.prototype.setLanguage = function (language) {
var m = this;
m.language = language;
return this;
};
Message.prototype.setText = function (ses, msg) {
var args = [];
for (var _i = 2; _i < arguments.length; _i++) {
args[_i - 2] = arguments[_i];
}
var m = this;
args.unshift(msg);
m.text = session.Session.prototype.gettext.apply(ses, args);
return this;
};
Message.prototype.setNText = function (ses, msg, msg_plural, count) {
var m = this;
m.text = ses.ngettext(msg, msg_plural, count);
return this;
};
Message.prototype.addAttachment = function (attachment) {
var m = this;
if (!m.attachments) {
m.attachments = [];
}
m.attachments.push(attachment);
return this;
};
Message.prototype.setChannelData = function (data) {
var m = this;
m.channelData = data;
return this;
};
return Message;
})();
exports.Message = Message;
41 changes: 38 additions & 3 deletions Node/lib/Session.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ var Session = (function (_super) {
this.args = args;
this.msgSent = false;
this._isReset = false;
this.lastSendTime = new Date().getTime();
this.sendQueue = [];
this.dialogs = args.dialogs;
if (typeof this.args.minSendDelay !== 'number') {
this.args.minSendDelay = 1000;
}
}
Session.prototype.dispatch = function (sessionState, message) {
var _this = this;
Expand Down Expand Up @@ -75,9 +80,8 @@ var Session = (function (_super) {
if (ss.callstack.length > 0) {
ss.callstack[ss.callstack.length - 1].state = this.dialogData || {};
}
this.msgSent = true;
var message = typeof msg == 'string' ? this.createMessage(msg, args) : msg;
this.emit('send', message);
this.delayedEmit('send', message);
return this;
};
Session.prototype.getMessageReceived = function () {
Expand All @@ -95,6 +99,9 @@ var Session = (function (_super) {
throw new Error('Dialog[' + id + '] not found.');
}
var ss = this.sessionState;
if (ss.callstack.length > 0) {
ss.callstack[ss.callstack.length - 1].state = this.dialogData || {};
}
var cur = { id: id, state: {} };
ss.callstack.push(cur);
this.dialogData = cur.state;
Expand Down Expand Up @@ -153,7 +160,7 @@ var Session = (function (_super) {
this.emit('error', r.error);
}
else {
this.emit('quit');
this.delayedEmit('quit');
}
}
return this;
Expand Down Expand Up @@ -221,6 +228,34 @@ var Session = (function (_super) {
}
return true;
};
Session.prototype.delayedEmit = function (event, message) {
var _this = this;
var now = new Date().getTime();
var delaySend = function () {
setTimeout(function () {
var entry = _this.sendQueue.shift();
_this.lastSendTime = now = new Date().getTime();
_this.emit(entry.event, entry.msg);
if (_this.sendQueue.length > 0) {
delaySend();
}
}, _this.args.minSendDelay - (now - _this.lastSendTime));
};
if (this.sendQueue.length == 0) {
this.msgSent = true;
if ((now - this.lastSendTime) >= this.args.minSendDelay) {
this.lastSendTime = now;
this.emit(event, message);
}
else {
this.sendQueue.push({ event: event, msg: message });
delaySend();
}
}
else {
this.sendQueue.push({ event: event, msg: message });
}
};
return Session;
})(events.EventEmitter);
exports.Session = Session;
Expand Down
90 changes: 90 additions & 0 deletions Node/lib/botbuilder.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,9 @@ export interface ISessionArgs {

/** Optional localizer to use when localizing the bots responses. */
localizer?: ILocalizer;

/** Optional minimum delay between messages sent to the user from the bot. */
minSendDelay?: number;
}

/** Signature of error events fired from a session. */
Expand Down Expand Up @@ -484,6 +487,9 @@ export interface IBotConnectorOptions {

/** Optional localizer used to localize the bots responses to the user. */
localizer?: ILocalizer;

/** Optional minimum delay between messages sent to the user from the bot. Default value is 1000. */
minSendDelay?: number;

/** Dialog to launch when a user initiates a new conversation with a bot. Default value is '/'. */
defaultDialogId?: string;
Expand Down Expand Up @@ -514,6 +520,9 @@ export interface ISkypeBotOptions {

/** Optional localizer used to localize the bots responses to the user. */
localizer?: ILocalizer;

/** Optional minimum delay between messages sent to the user from the bot. Default value is 1000. */
minSendDelay?: number;

/** Dialog to launch when a user initiates a new conversation with a bot. Default value is '/'. */
defaultDialogId?: string;
Expand Down Expand Up @@ -544,6 +553,9 @@ export interface ISlackBotOptions {

/** Optional localizer used to localize the bots responses to the user. */
localizer?: ILocalizer;

/** Optional minimum delay between messages sent to the user from the bot. Default value is 1500. */
minSendDelay?: number;

/** Dialog to launch when a user initiates a new conversation with a bot. Default value is '/'. */
defaultDialogId?: string;
Expand All @@ -553,6 +565,9 @@ export interface ISlackBotOptions {

/** Maximum time (in milliseconds) that a bot continues to recieve ambient messages after its been @mentioned. Default 5 minutes. */
ambientMentionDuration?: number;

/** Optional flag that if true will cause a 'typing' message to be sent when the bot recieves a message. */
sendIsType?: boolean;
}

/** Address info passed to SlackBot.beginDialog() calls. Specifies the address of the user or channel to start a conversation with. */
Expand Down Expand Up @@ -583,6 +598,9 @@ export interface ITextBotOptions {

/** Optional localizer used to localize the bots responses to the user. */
localizer?: ILocalizer;

/** Optional minimum delay between messages sent to the user from the bot. Default value is 1000. */
minSendDelay?: number;

/** Dialog to launch when a user initiates a new conversation with a bot. Default value is '/'. */
defaultDialogId?: string;
Expand Down Expand Up @@ -852,6 +870,47 @@ export class Session {
public createMessage(text: string, args?: any[]): IMessage;
}

/**
* Message builder class that simplifies building reply messages with attachments.
*/
export class Message implements IMessage {
/**
* Sets the messages language.
* @param language The language of the message.
*/
setLanguage(language: string): Message;

/**
* Sets the localized text of the message.
* @param session Session object used to localize the message text.
* @param text Text or template string for the reply. This will be localized using session.gettext().
* @param args Optional arguments used to format the message text when Text is a template.
*/
setText(session: Session, text: string, ...args: any[]): Message;

/**
* Loads the plural form of a localized string for the messages language. The output string will be formatted to
* include the count by replacing %d in the string with the count.
* @param session Session object used to localize the message text.
* @param msg Singular form of the string to use as a key in the localized string table. Use %d to specify where the count should go.
* @param msg_plural Plural form of the string to use as a key in the localized string table. Use %d to specify where the count should go.
* @param count Count to use when determining whether the singular or plural form of the string should be used.
*/
setNText(session: Session, msg: string, msg_plural: string, count: number): Message;

/**
* Adds an attachment to the message.
* @param attachment The attachment to add.
*/
addAttachment(attachment: IAttachment): Message;

/**
* Sets the channelData for the message.
* @param data The channel data to assign.
*/
setChannelData(data: any): Message;
}

/**
* Base class for all dialogs. Dialogs are the core component of the BotBuilder
* framework. Bots use Dialogs to manage arbitrarily complex conversations with
Expand Down Expand Up @@ -988,6 +1047,31 @@ export class DialogAction {
* @param steps Steps of a waterfall.
*/
static waterfall(steps: IDialogWaterfallStep[]): (session: Session, args: any) => void;

/**
* Returns a closer that wraps a built-in prompt with validation logic. The closure should be used
* to define a new dialog for the prompt using bot.add('/myPrompt', builder.DialogAction.)
* @example
* <pre><code>
* var bot = new builder.BotConnectorBot();
* bot.add('/', [
* function (session) {
* session.beginDialog('/meaningOfLife', { prompt: "What's the meaning of life?" });
* },
* function (session, results) {
* if (results.response) {
* session.send("That's correct! The meaning of life is 42.");
* } else {
* session.send("Sorry you couldn't figure it out. Everyone knows that the meaning of life is 42.");
* }
* }
* ]);
* bot.add('/meaningOfLife'. builder.DialogAction.validatedPrompt(builder.PromptType.text, function (response) {
* return response === '42';
* }));
* </code></pre>
*/
static validatedPrompt(promptType: PromptType, validator: (response: any) => boolean): (session: Session, args: any) => void;
}

/**
Expand Down Expand Up @@ -1635,6 +1719,7 @@ export class SlackBot extends DialogCollection {
* - reply: A reply to an existing message was sent. [IBotMessageEvent]
* - send: A new message was sent to start a new conversation. [IBotMessageEvent]
* - quit: The bot has elected to ended the current conversation. [IBotMessageEvent]
* - typing: The bot is sending a 'typing' message to indicate its busy. [IBotMessageEvent]
* - message_received: The bot received a message. [IBotMessageEvent]
* - bot_channel_join: The bot has joined a channel. [IBotMessageEvent]
* - user_channel_join: A user has joined a channel. [IBotMessageEvent]
Expand Down Expand Up @@ -1692,6 +1777,11 @@ export class SlackSession extends Session {
/** Data that's persisted on a per channel basis. */
channelData: any;

/**
* Causes the bot to send a 'typing' message indicating its busy.
*/
isTyping(): void;

/**
* Escapes &, <, and > characters in a text string. These characters are reserved in Slack for
* control codes so should always be escaped when returning user generated text.
Expand Down
2 changes: 2 additions & 0 deletions Node/lib/botbuilder.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
var session = require('./Session');
var message = require('./Message');
var dialog = require('./dialogs/Dialog');
var actions = require('./dialogs/DialogAction');
var collection = require('./dialogs/DialogCollection');
Expand All @@ -13,6 +14,7 @@ var skype = require('./bots/SkypeBot');
var slack = require('./bots/SlackBot');
var text = require('./bots/TextBot');
exports.Session = session.Session;
exports.Message = message.Message;
exports.Dialog = dialog.Dialog;
exports.ResumeReason = dialog.ResumeReason;
exports.DialogAction = actions.DialogAction;
Expand Down
4 changes: 3 additions & 1 deletion Node/lib/bots/BotConnectorBot.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ var BotConnectorBot = (function (_super) {
endpoint: process.env['endpoint'] || 'https://api.botframework.com',
appId: process.env['appId'] || '',
appSecret: process.env['appSecret'] || '',
defaultDialogId: '/'
defaultDialogId: '/',
minSendDelay: 1000
};
this.configure(options);
}
Expand Down Expand Up @@ -130,6 +131,7 @@ var BotConnectorBot = (function (_super) {
if (message.type == 'Message') {
var ses = new BotConnectorSession({
localizer: this.options.localizer,
minSendDelay: this.options.minSendDelay,
dialogs: this,
dialogId: dialogId,
dialogArgs: dialogArgs
Expand Down
4 changes: 3 additions & 1 deletion Node/lib/bots/SkypeBot.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ var SkypeBot = (function (_super) {
this.botService = botService;
this.options = {
maxSessionAge: 14400000,
defaultDialogId: '/'
defaultDialogId: '/',
minSendDelay: 1000
};
this.configure(options);
var events = 'message|personalMessage|groupMessage|attachment|threadBotAdded|threadAddMember|threadBotRemoved|threadRemoveMember|contactAdded|threadTopicUpdated|threadHistoryDisclosedUpdate'.split('|');
Expand Down Expand Up @@ -86,6 +87,7 @@ var SkypeBot = (function (_super) {
};
var ses = new SkypeSession({
localizer: this.options.localizer,
minSendDelay: this.options.minSendDelay,
dialogs: this,
dialogId: dialogId,
dialogArgs: dialogArgs
Expand Down
Loading

3 comments on commit 08fa258

@alapiere
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Stevenic ,
Thanks, this validatedPrompt is a game-changer for smart dialog flows !
But ..in order to setup a 'smart' bot, I'd like the bot to answer different messages in different error cases
something like

function(response) {
   if(!reponse.isNaN() {
      dialog.retryPrompt = "Error, the meaning of life must be a number"
      return false
   }else if( ..) {
      dialog.retryPrompt = ".."
      return false
   } 
   return true
}

how is it possible using this validatedPrompt ? (or : is retryPrompt modifiable from inside validator function ?)

@Stevenic
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's part of the reason I'm planning to replace validatedPrompt() with a whole new custom prompt system. You can't currently dynamically change the retry prompt like you'd like to. With custom prompts you'll be able to do anything you want. For now you can't use a validatedPrompt() to do what you want. You'll need to use a waterfall that calls Prompts.text() to get the users raw text input. Youll need to validate it in the next step of the waterfall and if it's invalid you'll need to reload your dialog using session.replaceDialog()

@alapiere
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was afraid you'd say so !
Well, this future customPrompt is for the better good then !

For now, it would be a bit cumbersome to handle this with a waterfall dialog : replaceDialog to the start would run all the welcoming sequence again, so I should set "stepX flags" to see if welcome message for each steps must be displayed ..
so I'll stick with a generic error message in the mean time :)

Please sign in to comment.