Musings of a geek.

2017-11-13
New York Marathon - 2017 edition

The New York City Marathon. Even today, having run it twice now, it still gives me chills. While there will always be people who will tell you Boston is better, to me, it’s not even close - New York hosts the best marathon on the planet. Granted, that’s based on a small sample size, and a strong New York bias, but I’m standing by my statement.

I honestly wasn’t planning on running it this year. No, I mean it. But when the last day of the lottery was announced, I couldn’t resist the draw, and threw my name in the hat, not at all expecting to be chosen. It was truly a “why the hell not”. And then I got that wonderful email stating I’d been selected! And, as an added bonus, my dear friend Susan decided she really wanted to do New York! She got in via a tour group, and joined me for the adventure.

So away I go to New York.

The training

I’m a firm believer that your performance at the marathon is a direct reflection of your training. While there are certainly good days and bad days, good weather and bad weather (see Grandma’s 2016), the number one indicator of your chances of success on Marathon Sunday (I’m hereby declaring that an official holiday) is how well your training went.

And my training was decidedly a mixed bag. On the plus side, while my speed didn’t come back quite as fast (pun intended) as I’d have liked, my hill training was super strong. One great advantage, although it doesn’t always feel that way on Saturday mornings, to running with Seattle Green Lake Running Group is you’re guaranteed hills, and lots of them. As a result, I was stronger on hills on Marathon Sunday than I’ve ever been, and that includes the days when I’d do the hill at Torrey Pines (those who’ve lived in San Diego know the one I’m talking about) on a regular basis.

I’d also had an amazing tuneup experience with my great running bud Karen at the Tacoma Narrows Half Marathon, which was hillier than either of us expected. While I missed my target time, I learned quite a bit about myself, and about running races, which I was able to put to good use on Marathon Sunday.

On the bad side, though, was the last 3 weeks of my training. Due to illness, I missed my 20 miler. And, due to my asthma kicking in, I missed the last week or so of runs. Someone once told me it’s better to go into a marathon slightly under trained. I was certainly going to be testing this theory.

The expo

I met up with Susan on Friday morning to go roam the expo, spend way too much money, and generally explore. We succeeded in all goals. As with any major marathon, the expo is crowded. But, this being the Javits Convention Center, which is huge, it doesn’t feel cramped, save for a few spots. Jackets were secured, recovery sandles from Oofos tracked down (they really are that comfortable), and general swag purchased.

Susan and I mugging it up

Susan and I mugging it up

The finish area

I’m a huge fan of doing the last couple of miles of any marathon. While I don’t want to do the bus tour, because I don’t want to see how damned far I’ll be going, I do want to see where I’ll be finishing so I have a good mental image of it. I find doing that makes it go by that much faster. Because we obviously didn’t want to walk that far, Susan and I, after catching the subway to Columbus Circle we grabbed a couple of Citi Bikes and pedaled counter clockwise back to the marathon’s entry point to Central Park. We then wanted to pedal back to the finish. The moment we turned around, it dawned on me that there’s only one acceptable way to bike around Central Park, and that’s counterclockwise. We sheepishly, and doing our best to avoid traffic, went the wrong way back to the start. We managed to only get yelled at twice by locals.

We made our way to the finish line, which was still in the process of being assembled. Sadly, the 26 mile sign wasn’t up yet, which meant Susan and I couldn’t get our standard picture. there. But, we did manage a selfie at the finish.

The finish line

We’ll be a little bit more tired tomorrow

The food

You can’t do New York without the food. While Susan and I were on our best behavior, avoiding alcohol save for one for lunch and dinner (because you have to have a little fun), we did find a couple of nice spots.

Pizza

We found pizza at Capizzi Hell’s Kitchen

Wine

And Susan had just one ridiculously large glass of wine

Marshmallows

This was dessert at our yakiniku place for dinner - I’m pretty sure marshmallows aren’t traditional (but they were tasty)

The ferry ride

If you’re a first timer, I am convinced that the only way to arrive at Staten Island on Marathon Sunday is via the ferry. Because, where else are you going to get to cruise by Lady Liberty on your way to the start line of a marathon? Unfortunately, the ride wasn’t quite as smooth as we’d have hoped. While we got the views, and comments on our robes, the ride took much longer than we expected, as we bobbed in the water for an extra 15 minutes or so. That, unfortunately made for a couple of challenges once we got to the start area.

In addition, the NYRR made some changes to the boat to bus area, where everyone was dumped into a funnel rather than into any form of an orderly queue. This led to a fair bit of confusion and jockeying for position in the line. This seemed to me to take longer than last year.

Lady Liberty

You’re looking lovely, Ms. Liberty

Robes on a boat

If you’re going to travel, travel in style in robes

The starting village

Becaues of the delays, and Susan’s need to hit bag check by 8:40, we needed to boogie to find the 2017 Dunkin Donuts hat. If you’re going to run New York, you need the hat. It’s a requirement. Rather than trusting my instincts and memory, since we’d already experienced one change from last year, I decided to ask information where the hats were. They pointed us towards the Orange area, which was on the opposite end from where Susan’s bag check. We trecked over there, only to discover that particular truck had run out. Dejected, we walked back to the bag check for Susan, and she dumped her bag. Not to give up, though, Susan and I ventured to an area where I’d seen the hats last year - success! (Of course, I managed to lose mine somewhere along the way. Susan, however, managed to keep hers.)

Hats

We found hats!

Both Susan and I had people we knew running, in particular a runner from Susan’s running group in Ottawa, and my cousin (who was running her first marathon). Fortunately, we managed to track them down.

Kanako

Kanako and Susan

Marian

Me and Marian

All of this running around finding hats and people ate up a fair bit of time. Tip: Always arrive earlier than you think you need to, so you have time to take care of everything, including (especially?) bathroom breaks. Between that and the delays getting there, Susan was a bit rushed to make her corral. She did, but she also didn’t have time to truly just relax. I bid Susan farewell, hung out with Marian for a litle bit, and hopped in my corral. There, in the most bizarre small world experience, I happened to meet the mother of a running friend from Seattle who was doing the race. I’m sure someone can calculate the odds of that.

The weather

As anyone who knows me knows, I obsess over the weather on race day. My perfect conditions would be about 45 degrees with sun, or about 55 with clouds (or some such combination there of). Marathon Sunday was showing mid-50s (perfect), and some form of rain. And when it came to rain, there was a chance of rain, at some point, of some amount, during the race. That was about as descriptive as any of the forecasts were; it just seemed difficult for them to pin down. Fortunately, as a Seattlite, I’m used to that type of weather.

The race strategy

I’d already decided to not chase after 3:59. I knew it wasn’t going to be there, and there wasn’t any sense in even trying. I knew if I did I’d have the same experience I had last year, where I walked the vast majority of the second half. I channeled my friend Christine, and focused on staying strong throughout the race. I wanted to eke out whatever my body had to offer on Marathon Sunday, and I figured that was a 4:15, with a “catch time” of 4:30 if I fell off my pace.

I’d also broken the course down into segments.

  • Up and over the Verrazano SLOW!!
  • Through Brooklyn to mile 8, again nice and easy, with a focus on seeing my brother Abram and his girlfriend Julia
  • Enjoy the brownstones of Williamsburg and navigate the Pulaski Bridge at the halfway point
  • Spend Queens prepping for the Queensboro Bridge (affectionately named the Queensboro Fucking Bridge, or QFB, as it punished me last year)
  • Conquer the QFB, exacting my revenge
  • Enjoy Manhattan part one, and seeing Abram and Julia
  • Take in the energy of the Bronx
  • Experience Harlem
  • Tackle the hill at Mile 23, again seeing my personal cheering station
  • Central Park and tears

The Verrazano

I’d spotted the 4:15 pacers in my corral. Perfect. In theory, they’re trained and ready to do this. Unfortunately, they were a little disorganized, as one decided to head out to the bathroom without telling the first one. As a result, she was panicked (rightly so), and I made the decision to just focus on myself, heading out without them, figuring they’d catch me a bit later in the race.

Just as the gun went off it started to spritz just a little bit. No rain, just spritz. My only concern was that it would tamp down the crowds if it started to come down harder. I remained focused.

Slow and steady wins the race, and up the bridge I went nice and easy, and just enjoyed the longest downhill of the race on the other side. Everything went perfectly according to my plan, as I found myself about 20 seconds behind pace, which I could easily make up along the way. A couple of quick turns through Bay Ridge in Brooklyn, and up 4th I went, enjoying the crowd and experience, and still staying within myself.

Through Brooklyn to mile 8

The run up 4th is a fantastic stretch, with plenty of crowds to cheer you along. The mist wasn’t impacting the crowds, and I was just soaking it all in. I was taking back the little bit of lost time I had from the bridge bit by bit, and around mile 6 the 4:15 pacer caught me, which I was expecting. She was solo, her partner nowhere to be seen. I tracked her, but didn’t fall in behind the group directly, as weaving through the crowds was an issue - I just didn’t want to burn energy doing that.

Into the heart of Brooklyn, left turn, right onto Lafayette, and there were Abram and Julia, signs in hand, to cheer me on. At this point I was relatively close to the 4:15 time I was hoping for, felt full of energy, and was excited for the brownstones and Williamsburg.

Feeling strong in Brooklyn

I’ve got this

Williamsburg and the Pulaski Bridge

Sadly, the mist was tamping down the usual general life of Williamsburg, which is one of the best features of the New York Marathon course. So while I didn’t have that, I did have the brownstones to enjoy, which I did. Also, since I was feeling strong, I wasn’t focused on how I was going to maintain pace.

Then, around mile 9, the other 4:15 pacer caught up! As suspected, he did stop by the restroom, and had worked to catch up to his teammate. Now, maybe he did a good job of slowly doing this, but I couldn’t help but think that anyone who followed him, as he started further back than the 4:15 pacer I was tracking, was now a good bit ahead of where they should be. Either that, or the original pacer was behind where she needed to be.

He caught up to her, and they had a little bit of an argument, as she needed to find a restroom. She handed him her sign, ran off to take care of business, and then caught back up. Once she did, the two of them got to chatting and running way too fast. As I kept seeing a 9:20 pace on my watch, I decided I was just going to fall back and let them go.

Approaching the Pulaski bridge, I realized it was a bit more of a hill than I remembered. Not to worry, I told myself - I eat hills for breakfast, and bridges for lunch. I’d trained for this. And I settled in, worked my way up it, hitting the 13.1 mark, and found myself in Queens. Still, feeling strong.

Queens

Of all of the boroughs, Queens is my least favorite. I’m sorry, Queens, but there’s just not much that the course covers there. The fans are nice, though.

Anyway, it was time to focus on getting ready for the bridge. There’s a song called Gimme Chocolate by Baby Metal, a J-pop metal band, that has probably the catchiest hook ever. For whatever reason, this is a song that just sticks in my brain, and it’s what helped me conquer hills at night during Ragnar. It was going to help me conquer the QFB. The night before I’d queued up the song, tested my bluetooth headphones, and made sure that when I clicked play it would start properly. Mind you, I basically never run with music. But I knew I wanted something for the QFB.

Setup time. Pulled the headphones out of my Flipbelt, put them on, clicked play, and I heard the strains of the song in my ears. Perfect. Hit pause, and enjoyed what Queens had to offer, started to get excited for my chance to get revenge on the bridge.

As for the weather at this point, the mist continued.

Queensboro Fucking Bridge

Here we go. This is it. I started out on the bridge, hit play, turned up the volume, and began my attack. Literally 20 meters in I knew I had it. My legs were super strong. My spirits were high. My music was giving me that extra energy that I needed. And, with the knowledge I wouldn’t be able to see the top of the climb due to the upper deck of the bridge, I focused instead on listening to the song twice.

About 200 meters in I felt a rush that everything I’d focused on was coming to fruition. 200 meters later, I fought back tears. As I passed people, as I saw people pulling off to the side, knowing I was there last year, I gained even more energy. When I reached the crest, I had my “Fuck Yeah!“ moment. Certainly, not on the same level as Shalane, but like my hero it was something I’d been defeated by in the past, had worked towards, and conquered.

Down the other side, and I was the guy that yelled at the top of his lungs, “WELCOME TO MANHATTAN!”

A little bit of hindsight here. I feel like in the end I expended too much energy, physically and emotionally, here. This was what I had built up in my head as my arch-nemesis, and I took it down. While I was right around the 4:15 time I wanted at this point, maybe about 2 minutes back, the energy I spent here cost me later in the race.

Manhattan, part 1

There are truly no words to describe how amazing the fans are after you come off the QFB, and for the next 3 miles thereafter. It’s electric. Add to that the fact that it’s generally flat, and even downhill at times, and it’s easy to take off. I maintained my focus, and my pace. I kept within myself. I got lucky and saw the friend who’s mother I met at the start area. Life was amazing.

Now, when it comes to having someone cheer you at the New York Marathon, you need to plan well ahead, knowing exactly where they are. I’d worked out with Abram and Julia that we’d see each other at 90th St. Try as I might, I could not spot them in the crowd. However, they’d made friends with a couple of teenagers next to them, who spotted me and screamed “JERSEY!!!!”. I pointed in the direction of them, and Julia captured this picture. They insist I saw them. All I have to say is this - #fakenews.

Pointing at screaming kids

Pointing but not seeing anyone

Shortly after seeing them, my energy deflated. I hadn’t hit a wall, per se, but I had slowed down. I found myself doing 11 minute miles. I walked water stations. I wasn’t defeated by any stretch, but I just had less gumption.

Around mile 19, the 4:30 pacers went past me, which is around a 10:45/mile pace. I looked at my watch, which said I was currently at a 10:15 overall pace. I guess there’s a pack of 4:30 marathonners who set an amazing PR.

And, the drizzle continued.

The Bronx

To get into The Bronx, you need to go across the Willis Avenue Bridge. This bridge is short, cambered, and a steep incline - the stuff that runner nightmares are made of. I walked part of that bridge, but managed to run the bulk of it. Upon entering the Bronx, I was greeted by the good people who call that borough their home, and who truly take pride in it. Sadly, there were fewer of them, as the drizzle was still a thing. But those who were there embraced us warmly. A couple of rights, a couple of lefts, a turn towards the west, and I found myself, much quicker than I expected, heading back into Manhattan.

Let’s talk a little bit about fueling. I use PowerBar Gels, and they generally do me rather well. I pop one every 4 miles, starting at mile 3. The one at mile 19 did not go well. It nearly came right back up. Uh-oh. I still have 7 miles left to go, and I can’t do this sans fuel. But I also knew there wasn’t any chance I’d be able to do another gel. So, I made the absolulte worst decision any marathoner can make - I tried something new on race day. When I hit The Bronx, I started drinking the Gatorade they were handing out, and I ate a piece of a banana. I have never eaten on a run. Amazingly, it didn’t come back to haunt me. Don’t try this at home, kids.

Manhattan 2, Harlem Boogaloo

I’ve yet to explore Harlem, save for running through it, and it’s something I truly want to fix. Until that time, though, I’ll have yet another wonderful memory of the streets being lined with fans, a wonderful band and dance crew, and a jog around Marcus Garvey Memorial Park, and there’s Central Park ahead on the right. A glorious sight.

The odd sight? That would be the 4:10 pacer cruising by me. Where he was going I’ll never know.

Mile 23

Mile 23 is infamous for being the one last big climb, and now famous for being the spot where Shalane threw in one last surge that her competitors just wilted under. As I said before, I had focused quite a bit on hills during my training, and it paid off for the entire race. Mile 23 was no exception - left, right, left, right, and HEY! - there’s Abram and Julia! High-5s all around. A little bit more climb, a right into Engineer’s Gate and Central Park, and there’s only one thing left to do: Finish the 2017 New York City Marathon!

High 5s!

High 5s all around!! Almost there!

Central Park and Tears

I’m honestly tearing up just typing this. There are truly no words that can describe the rush of emotions of entering Central Park. You’re down to under 5K left. Less than 30 minutes to go. And, for me, Central Park is my absolute favorite place to run in the world, my happy place.

Between the rush of hitting The Park, and seeing Abram and Julia, I knew I had everything in me to finish without having to walk through the water stations. Granted, these were 12 minute miles, but I was in fact running.

Saw the Cougar. Enjoyed the trees. Took in the cheers from fans lining the streets. Found our way out of Central Park, along Central Park South, and then the right turn back into the park to finish the New York City Marathon. That little spike they throw at you for the .2 of 26.2? Yeah, that had nothing on me this time. This race was mine.

I finished the New York City Marathon.

4:37:44.

A few minutes past my “catch time”, but I’m very proud of where I wound up regardless. I know I got almost everything out of my body that day, and only made a couple of strategic mistakes (besides the eating).

And, most importantly, the rain stopped.

The Exit and Reunion

I had my medal hung on me, and gave it a big kiss. Grabbed the foil. Got the recovery bag, and ate my apple - the best apple ever.

It’s a long walk to get out of Central Park, and uphill. One could argue that is truly the worst hill of the course, and they’d be right.

Exit onto Central Park West, and a volunteer put on my poncho, as my hands were full holding my bag, phone, and second foil. As I walked away I was still a bit cold, which another volunteer noticed. She asked if she wanted her to put my hood up. I said yes, and she did. I walked away thinking that was the absolute sweetest thing anyone had ever done for me, and absolutely broke down crying. Sobbing. Thus is the power of the marathon.

I greeted Susan, who PR’ed, Abram and Julia each with a huge hug. Abram handed me my first post race beer, which is always the best beer in the world.

Post Race Beer

Best. Beer. Ever.

In Retrospect

Nearly two weeks separated from the race, and the emotions are all still fresh, as are the memories. This is truly the greatest marathon on the planet, and one I will absolutely run again in the future. If you ever have the opportunity, take it. Absolutely take it.

As for my race, I made two key mistakes that cost me time. The first was keeping pace for a couple of miles with the way too fast 4:15 pacers, which sapped some energy out of me, between the speed itself and the weaving. The second was my singular focus on the QFB. By the time I was over the bridge, I’d burned so much energy, energy that I really needed for the last 10 miles. I should have been able to eke out a few more 10 minute miles before slowing to the 12 minute pace I had towards the end, which would have given me the 4:30 I was hoping to achieve.

BUT, I finished feeling strong, probably stronger than I’ve felt finishing any marathon, including my PR. I was able to apply the lessons learned from the Tacoma Narrows Race, embracing the suck, and not giving in, even when I wanted to (which, oddly, wasn’t that often). While my speed certainly isn’t back, I’m stronger emotionally than I’ve ever been, and better on hills than I ever thought I could be.

I read the other day that often the marathon beats you. Marathon Sunday saw me battling it to a solid draw.

Next stop - Vancouver Marathon and #breaking4.

Happy Susan

Celebrating with Susan the next morning

The Finish

The Finish
閱讀本文

2017-10-29
Managing versions and timeouts in Bot Framework

Creating bots in the real world brings additional challenges with versioning and user behavior. Obviously, when you update your bot, you want to make sure you seemlessly migrate users over to the new bot, often allowing them to complete the conversation they’re currently having before showing them the new bot. Also, because Bot Framework does not time out users, you can also run into a situation where a user has left for a sizeable amount of time, and either state has changed or you simply want to offer the user the chance to start over.

Let’s break this down by first talking about how things work behind the scenes inside of Bot Framework, introduce middleware, and then put forth a couple of patterns you can implement to handle these situations.

Well, one quick note before we get started - this is a 300 level concept, so I’m making the assumption you’re already familiar with Bot Framework, and have created a few bots. If not, you can check out the Bot Framework MVA.

Bot Framework behind the scenes

A bot is, at the end of the day, simply a web service. Web services, of course, are stateless, meaning that after the message is sent from the user, and a reply returned, the connection is broken down, and all data is lost. When working with a bot, however, this poses a problem, because an operation will typically take place across multiple round trips from the user to the bot.

To handle this, Bot Framework “dehydrates” or serializes the current state of the bot for each user. When the user returns by sending a new message, the user state is “rehydrated” or reloaded, the bot determines where the user left off, and the next function is then run. You can see this happening behind the scenes when tracking your bot in the console, seeing the waterfall in action, and which step the bot is about to call. In a simple “Hello, world” bot, the user would send hello, the bot sends the prompt for the name, the user replies, and then the bot runs the next function - in this case, sending the hello message.

1
2
3
4
5
6
7
8
9
10
11
const bot = new builder.UniversalBot(
connector,
[
(session) => {
builder.Prompts.text(session, 'What is your name?');
},
(session, results) => {
session.endConversation(`Hello, ${results.response}.`);
}
]
);

No timeout

Because we’re dealing with humans, who may take some time to respond back to the bot, there is no timeout for a session[1]. If a user leaves for a couple of hours, and then replies to the What is your name? question, the bot simply picks up where it left off, as if the user replied instantly.

The first challenge here is state may have modified - the price for an item may have changed, a reservation time may no longer be available, or the operation may simply no longer be valid. In addition, this might confuse the user in general - if you didn’t talk to a bot for a couple of days, and potentially had the chat history on your client cleared, you might not expect the bot to pick up, or even know, where you left off.

The second challenge is when updating the bot. If you modified a dialog, the bot might not be able to pick up where it left off seamlessly. You may have removed a step from a waterfall, meaning the bot will error, now knowing what to run. Or, if a step changed, it may ask the user a question that simply doesn’t make sense for the current flow.

Addressing these issues doesn’isn’t necessarily difficult, but it does take a little bit of forethought and planning. With a little bit of middleware, you can transition as needed.

Middleware

Middleware allows you to intercept messages before they’re passed into the dialog flow, where the bot processes them as normal. This is useful for logging, rerouting messages (say for bot to human handoff), or, in our case, managing how a user is directed through the dialogs offered by the bot.

To implement middleware, you call use, passing in an object that implements at least one of three methods:

  • receive, which fires when a event is received by the bot
  • send, which fires when an event is sent by the bot
  • botbuilder, which executes after receive and the message is bound to a session

The last part of botbuilder is key in our case, because it’s going to allow us to open up where the user currently sits in the flow of the dialog(s), and determine the next step.

Strategies

Let’s walk through a few strategies for managing timeouts and updating your bot. I’m not going to walk through every possibility, but rather show off a few representative solutions that will hopefully illustrate how to approach the problem, and provide enough information for you to implement what’s needed.

Adding a timeout

As mentioned above, a bot doesn’t timeout. As a result, if it’s been even a few days since the user last replied, the bot is going to attempt to pick up where it left off. This can either confuse the user, or operate on bad information, neither of which is a good thing.

Reset the conversation

The first potential solution, and the easiest to implement, is to simply reset the conversation. We’ll do this by adding a lastAccess property to userData. We’ll then query this to see how long it’s been since the user last talked to the bot, and, if necessary, reset the conversation.

We determine if the user is currently in a conversation by taking a look at the dialogStack and determining if there is at least one dialog there. Our threshold is 10 seconds, which is obviously artificially small, but I want you to be able to easily test the implementation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
bot.use(
{
botbuilder: (session, next) => {
// grab the current time
const currentTime = Date.now();
// see how long it's been (in milliseconds) since the user last
// accessed the bot
const lastAccessGap = currentTime - session.userData.lastAccess;
// update the last access
session.userData.lastAccess = currentTime;
// see if there is a dialog and it's been greater than 10 seconds
// since the user last talked to the bot
if(session.dialogStack().length > 0 && lastAccessGap > 10000) {
// a couple of friendly messages
session.send(`Hey there! It looks like it's been a while since you were last here.`);
session.send(`Let's start over to make sure we're both on the same page.`);
// reset the conversation, sending the user to the root dialog
session.reset('/');
} else {
// allow the flow to continue as normal
next();
}
},
}
)

Ask the user what they want to do

The next possibility, and slightly more advanced, is to ask the user what they want to do. We’ll use the same timestamp technique as above, but this time we’ll route the user to a confirmation dialog. If they say yes, we’ll end the dialog, which will reprompt the user. If they say no, we’ll reset the dialog like before. Again, the threshold is artificially small for testing purposes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
bot.use(
{
botbuilder: (session, next) => {
// grab the current time
const currentTime = Date.now();
// see how long it's been (in milliseconds) since the user last
// accessed the bot
const lastAccessGap = currentTime - session.userData.lastAccess;
// update the last access
session.userData.lastAccess = currentTime;
// see if there is a dialog and it's been greater than 10 seconds
// since the user last talked to the bot
if(session.dialogStack().length > 0 && lastAccessGap > 10000) {
// begin the confirmation dialog
session.beginDialog('confirm-continue');
} else {
// allow the user to continue as normal
next();
}
},
}
)
bot.dialog('confirm-continue', [
(session, args) => {
// prompt the user
builder.Prompts.confirm(session, `It looks like you've been gone for a little while. Do you want to continue where you left off?`);
},
(session, results) => {
if(results.response) {
// redirect back to the original dialog
// the last active prompt will be re-run
session.endDialog(`Sounds good. Here's the last question I asked you:`);
} else {
// reset the conversation
session.send(`Sounds good. Let's start over.`);
session.reset('/');
}
}
]);

Some thoughts on timeouts

It’s really up to you to decide how to best manage timeouts and what works best for your bot. You might decide to time the user out after twenty minutes, or maybe a couple of hours. You might automatially reset, especially in simpler two or three turn dialogs, or to prompt the user for more complex bots. You might even go a couple steps further, detecting which dialog the user is in, and make the decision based on that.

Managing versions

Versioning a live bot is a little trickier. You want to get users onto the new bot as quickly as possible, but you probably don’t want to interrupt the current flow of a user already in the middle of a dialog. If nothing else, when you version your bot, you need to handle the fact that a user in the middle of a dialog might be returning to a dialog step that doesn’t exist, or a dialog that’s changed profoundly, meaning the message the user sent might not make sense in the flow you deployed.

Reloading an older version

If you do want to transition the user, you will need to pay a little bit of attention to how you update your bot. In theory, you could do this on a dialog by dialog basis, but the code becomes trickier as you need to figure out where the user is, and swap out the dialog. I find it much easier to simply swap out the entire bot. To help manage this, I’ll put the bot, and its associated dialogs, into a folder. When it comes time to update it, I’ll copy the entire folder structure, update the copy, and then update app.js to call the updated (new) bot.

When you need to send a user to the old bot, you simply update the library property of the session, which sets the bot that will run for the incoming message. In the code snippet below, I have both an updated and original bot in the same file for simplicity’s sake. I hope you can see from here how you would implement this with multiple folders.

In the sample below, I’m going to transition the user once their current conversation completes. You could, if you so desire, update this code to always reset the conversation, with a message of course, which would cause the user to be rerouted to the new bot. Also, the code could be streamlined a little bit, but breaking it apart like I’ve done makes it (I think) a little easier to explain with comments.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
const connector = new builder.ChatConnector();
const bot = new builder.UniversalBot(
// this is the updated bot
connector,
[
(session) => {
builder.Prompts.text(session, 'What is your name?');
},
(session, results) => {
session.endConversation(`Hello, ${results.response}, from the updated bot`);
}
]
);
// set a version number using duck typing
bot.version = 2;
const getOriginalBot = (connector) =>{
// this will return the original bot
const originalBot = new builder.UniversalBot(
connector,
[
(session) => {
builder.Prompts.text(session, 'What is your name?');
},
(session, results) => {
session.endConversation(`Hello, ${results.response}, from the original bot`);
}
]
);
// set a version number by using duck typing
originalBot.version = 1;
return originalBot;
};
bot.use(
{
botbuilder: (session, next) => {
if(!session.userData.version) {
// new user. set the version
session.userData.version = bot.version;
}
// check if user is in conversation and
// if the user is on the prior version
if(session.dialogStack().length > 0 && session.userData.version < bot.version) {
// load the original bot
session.library = getOriginalBot(connector);
// pass control to the original bot
// the correc step and dialog will be executed
next();
} else {
// update the version in userData
session.userData.version = bot.version;
// pass control to the new bot
next();
}
},
}
);

If you wanted to test this, start by making the new bot version 1 and sending a message to get yourself into the dialog. Then update the version to 2, rerun the application while not restarting the conversation in Bot Emulator, and sending the second message. You should see the original bot message.

Some thoughts on versioning

Versioning is tricky, especially if you’re in a CI/CD environment where you expect to make multiple updates to the bot. Bot Framework (unfortunately?) doesn’t automatically handle transitions for you, so you will need to decide what pattern will work best for you. This is also important when developing and testing your bot in a channel such as Skype or Slack, where how to reset the state from the client isn’t overly obvious. In dev you could certainly skip the transition, but in production you’ll likely want to implement a mechanism for doing so.

Final thoughts

Once you introduce your bot to the “real world”, things change. Management of the experience becomes much more important, and dealing with timeouts and versioning allows you to ensure a good experience for your users. This is something you should thing about early on, as it’s easier to introduce this at the beginning than to an existing, complex bot.

There are numerous ways you could handle this. Hopefully I’ve provided enough of a foundation for you to create an implementation that’s right for your bot.

[1] I am referring to session in general, not to the session object in Bot Framework.

閱讀本文

2017-07-25
Getting running in Seattle

Welcome to Seattle, land of more trails, and rain, than you can imagine! As you look out over the landscape, you might be wondering where to get started, where to run, and where all of the other runners are. Hopefully, this little post will help you out.

One caveat before we get going here - I am a road running marathoner. As a result, I don’t have any great insights on trail running in the area. I know it’s plentiful, and there are plenty of trail runners around. I’m just not one of them.

General Seattle notes

First and foremost, let’s talk about the wet elephant in the room - the rain. Seattle has a mostly deserved reputation for having a lot of rain. As a result, you will need to run in the rain. When I moved here from San Diego, this took a little adjusting for me.

That said, it does not rain nearly as often as the media implies. In addition, it’s rarely real rain; it’s mostly a hard drizzle. It’s pretty easy to just deal with. You will notice most Seattle runners eschew a rain jacket, as all they generally do is lock in body heat and sweat. Plus, you can only get so wet.

The one thing you might not realize if you’re new to Seattle is the hills. There are hills basically everywhere, and the majority of races in town will have some good climbs. There’s no avoiding it, and really - you don’t want to avoid it. Hills make you stronger. If you plan on doing any of the races here you’ll need to train hills.

Big races

The surrounding region features races nearly every weekend, or at least it seems that way. If you decide on a random Sunday morning you want to add a new medal to your collection, chances are you’ll be able to do it.

The two big races are the Rock ‘n’ Roll Seattle Marathon in June, and the Seattle Marathon in November. The former is a bigger race, with June being a more desirable month to run, and of course the marketing power of Rock n Roll. Both courses are similar, and have been recently redrawn due to construction on I-90, and are hilly. You will certainly be tested in those races.

Rail Trails - Burke Gilman (The Burke) and Sammamish River Trails

Totalling over 30 miles of (mostly) uninterrupted paved rail trail, the Burke Gilman and Sammamish River trails make up some of my favorite areas to run in King County. They offer miles upon miles of wonderful views, nature and tree-lined goodness. Because they’re rail trails, they are bone flat, so you certainly don’t want to make it your regular route as it won’t serve you well come race day. But they’re a wonderful treat, and extremely popular with the locals.

One thing to note about The Burke is there is one section where the trail is broken by an industrial neighborhood in Ballard. It looks like they’ll finally be completing it, but until then you will have a little bit of neighborhood running to deal with depending on where you are.

Alki Trail

As a West Seattleite, Alki Trail is my backyard, and one of my favorite spots. You can run from the southern tip of Lincoln Park, through a couple of cute neighborhoods, along the beach area, and all the way to downtown Seattle if you feel like crossing the bridge. The run will treat you to wonderful views of Seattle and the sound, fantastic people watching on summer weekends, and the occasional view of Mount Rainier.

Green Lake

Easily the most popular route in Seattle, Green Lake boasts a 2.8mi inner trail (and just over 3mi on the outer trail), which is perfect for a quick jog. Green Lake has a similar vibe to a much smaller Central Park, and the same walker congestion to match - you will be weaving on busy days. That said, the people watching and the feel draws me out there on a regular basis.

Lake Union

The second most popular route according to Strava, the lap around Lake Union is just over 6 miles. The route will treat you to views of the lake, and a few charming neighborhoods. There are a couple of hills on the path, but it’s still pretty flat by Seattle standards.

Seattle Green Lake Running Group

There are a few different running groups in Seattle, but the only one I have any experience with, and easily the most vibrant, is Seattle Green Lake Running Group. Offering runs every day of the week (sometimes twice), SGLRG is a community driven group which fits almost any runner’s needs and schedule. You can find tempo runs on Wednesday mornings, speed work or hill repeats on Mondays, and long runs on Saturdays, which is by far the most popular run for the group. The Saturday run starts at Green Lake, and ventures out into the neighborhoods from there; I’ve been able to get a wonderful tour of Seattle during my time with SGLRG. If you’re looking to join, the best thing to do is to post a distance and your pace on the Meetup page for the Saturday morning run, and jump right in.

Got any other tips? Feel free to comment below! I’d love to hear them.

閱讀本文

2017-07-12
Create bots with TypeScript

Whenever I’m doing demos or other presentations I work very hard to keep things as real world as possible. While there will certainly be little “cheats”, such as keeping all items in a single file, or having snippets already available, to help make the demo easier to digest, there’s one big lie I wind up telling every bot presentation I do. I do all my demos in JavaScript, even though I create all my bots using TypeScript.

Why the lie?

Whenever I’m talking about how to build a chat bot using Bot Framework, I’m trying to demonstrate how you can do so using skills you already have. If you know JavaScript, and have even a cursory understanding of Node.js, you can get up and running relatively quickly with your first bot. The last thing in the world I want to do is throw another hurdle in front of attendees, like learning another programming language.

Even though I don’t want to put another hurdle in front of someone, the thing about TypeScript is it turns out it’s not much of a hurdle after all.

What is TypeScript

If you’re not already familiar with TypeScript, it’s a language headed by Anders Hejlsberg, whose claims to fame include Delphi and C#. TypeScript is designed to help overcome the various shortcomings of JavaScript, and do so in such a fashion that allows you to begin using cutting edge features, such as async/await, which aren’t globally available in ECMAScript.

TypeScript is designed to build on the existing syntax of JavaScript; in fact, 70% of the syntax in TypeScript is simply JavaScript. This helps make the transition to the language smoother for experienced JavaScript developers. In a lot of ways, TypeScript is a combination of Java and JavaScript.

TypeScript offers many features you expect from programming languages, such as static (or strong) typing, OOP, and better module management.

One key about TypeScript is it transcompiles into JavaScript, and to the version of JavaScript/ECMAScript you need. So you can write in TypeScript, take advantage of the features the language offers, knowing the resulting code will run in the environment you’re targeting.

Why use TypeScript when creating bots?

One of the biggest weaknesses of JavaScript is the inability to declare the type of a variable. This limits the amount of support an IDE, such as VS Code, can offer you. Below is the code one might have in a separate file for a dialog in a bot:

1
2
3
4
5
module.exports = [
(session) => {
session.endConversation('Thank you for your input!');
}
]

If you put that into a JavaScript file, you would notice when hitting dot after session, VS Code wouldn’t show you endConversation as an available option, or if it did, it’s because it’s seeing it from another file in your project. The IDE has no way of knowing that session is actually of type builder.session.

Contrast that with the following bit of TypeScript, where we are able to identify the type:

1
2
3
4
5
export default [
(session: builder.Session) => {
session.endConversation('Thank you for your input!');
}
]

When you create the TypeScript file, you’ll notice VS Code knows exactly what the session parameter is, and is able to offer you IntelliSense.

Getting started with TypeScript

Installing TypeScript

In order to start programming in TypeScript, you will need to install TypeScript. TypeScript is available as an NPM package, and you can simply add it as a developer dependency to your project. Personally, because I use TypeScript extensively, I install it globally.

1
npm install -g typescript

Creating and configuring the project

If you use the Bot Framework Yeoman generator I created you will notice there is a template already available for TypeScript. For purposes of this post, we’ll create everything from scratch, so you can see how it is all brought together.

Add packages and dependencies

Create a new folder, and add a package.json file with the following contents:

1
2
3
4
5
6
7
8
9
10
11
{
"name": "type-script-bot",
"dependencies": {
"botbuilder": "^3.4.4",
"restify": "^4.3.0"
},
"devDependencies": {
"@types/node": "^6.0.52",
"@types/restify": "^2.0.35"
}
}

The dependencies section is pretty standard for a bot, but you might not be familiar with the devDependencies. The devDependencies contain the types of the various packages we’ll be using. Types are the various interfaces for the objects and classes in a particular package. So @types/restify contains the interfaces provided by restify. This will add IntelliSense to the project. In the case of botbuilder, we don’t need to add a types file, as the framework is written in TypeScript, and contains all of the necessary types. After saving the file, run the installation process like normal.

1
npm install

Configuring TypeScript compilation

As mentioned before, TypeScript will be transcompiled to JavaScript. You configure how this occurs by using a tsconfig.json file.

The module and target options tell the transcompiler to emit JavaScript that is compliant with ES6. outDir specifies where the JavaScript files will be output. The files section identifies which files will be transcompiled. Add a file named tsconfig.json with the following content.

1
2
3
4
5
6
7
8
9
10
11
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"outDir": "./built"
},
"files": [
"dialog.ts",
"app.ts"
]
}

Creating the bot and dialog

Creating the dialog

Let’s start by creating a basic dialog. Add a file to your project named dialog.ts, and add the code you see below. You will notice this is standard Node.js bot code, with a couple of differences.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// dialog.ts
import * as builder from 'botbuilder';
interface IResults {
response: string;
}
export default [
(session: builder.Session) => {
builder.Prompts.text(session, 'What is your name?');
},
(session: builder.Session, results: IResults) => {
session.endConversation(`Hello, ${results.response}`);
}
]

From the top, you’ll notice the import statement is different than in JavaScript. Rather than using require, you use the import command. The * means you’ll be importing everything from the package, and as allows you to identify an alias. The end result is effectively the same as using const builder = require('botbuilder');, like you would have done traditionally.

Second, you’ll notice the creation of the interface IResults. You added a single property named response, and marked it as type string. Interfaces, just as in C# or Java, give you the ability to identify the structure. In the case of TypeScript, interfaces are completely weightless; they are not compiled to the JavaScript. Interfaces help give you a better development experience.

Rather than using module.exports to export the array that contains your waterfall, you use export default, followed by the array. The syntax is slightly different, but the results are the same.

Finally, you’re declaring the data type of session and results on each of the waterfall steps to aid your development experience. results is using the interface you created earlier in the file. You’ll notice when you do this, and you start typing response.results, VS Code will provide IntelliSense, and show you response as an available property of type string.

Creating the bot and host

The code to create the bot will be similar to what you would typically do with JavaScript. The main difference you’ll notice is the way packages and other items are imported. Add a file named app.ts with the following code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// app.ts
import * as restify from 'restify';
import * as builder from 'botbuilder';
import dialog from './dialog'
const connector = new builder.ChatConnector({
appId: process.env.MICROSOFT_APP_ID,
appPassword: process.env.MICROSOFT_APP_PASSWORD
});
const bot = new builder.UniversalBot(connector, dialog);
const server = restify.createServer();
server.post('/api/messages', (bot.connector('*') as builder.ChatConnector).listen());
server.listen(process.env.PORT, () => console.log(`${server.name} listening to ${server.url}`));

As you enter the code, you’ll notice that VS Code is able to offer you support throughout the entire process. Because every variable has a type that VS Code can identify, it’s able to provide IntelliSense, unlike when using JavaScript, where the level of support will vary.

Running the code

In order to run the code, it will need to be transcompiled. You can do this by simply running tsc. Because we created a tsconfig.json file, the transcompiler will know what to transcompile, and how to do it. The watch switch will automatically detect changes to the TypeScript files, and transcompile on the fly.

1
tsc --watch

If you take a look at the JavaScript files, you’ll notice they’re relatively similar to what was created in TypeScript. A couple of key differences you’ll notice is the interface we created wasn’t emitted, and the type declarations for parameters are also not part of the JavaScript file that was created. Running the bot is done just as you normally would, using either node or nodemon.

1
node app.js

Next steps

There’s quite a bit that I left on the table when it comes to TypeScript. We could make better use of interfaces with our dialogs, we could create a class for our app, we could …, we could …. My goal with this post was to help get you up and running with TypeScript, and show some of the power that’s made available.

If you want to know more about TypeScript, you can check out the official site, or an edX course.

閱讀本文

2017-06-08
Working with custom buttons to drive conversations

If I’ve said anything about bots, it’s that they’re apps. They’re just apps with a conversational interface. This style of interface can be extremely powerful, as it allows the user to better express themselves, or “skip to the end” if they already know what it is they’re trying to accomplish. The problem, though, is without a bit of forethought to the design of the bot it’s easy to wind up back in this scenario, where the user isn’t sure what to do next:

Command Prompt

If you’re well versed in the set of commands you can quickly perform any operation you desire. But there is no guidance provided by the system. Just as they’re no guidance provided here:

Command Prompt

Buttons are a good thing

We need to guide the user.

Buttons exist for a reason. They succinctly show the user what options are available, and can guide the user towards what they’re looking for. In addition, they help reduce the amount of typing required, which is especially important when talking about someone accessing a bot on a mobile device with a tiny keyboard.

Providing choices

The most obvious place where buttons shine is when providing a list of choices for a user to select from. This might be a shipping method, a category for filtering, or, really, any other set of options. To support a list of choices, BotBuilder provides a choice prompt. The choice prompt, as you might expect, provides the user a list of options for them to choose from, and then provides access to that in the next step of the dialog.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// sample waterfall dialog for a choice
(session, args, next) => {
builder.Prompts.choice(
session,
`What color do you want?`,
['Red', 'Green', 'Blue'],
{
listStyle: builder.ListStyle.button,
retryPrompt: `Please choose from the list of options`,
maxRetries: 2,
}
);
},
(session, results, next) => {
if(results.response && results.response.entity)
session.endConversation(`You chose ${results.response.entity}`);
else
session.endConversation(`Sorry, I didn't understand your choice.`);
}

The choice prompt limits the user’s response to just the list of options you provide. You can limit the number of times the bot will ask the user for a response before moving onto the next step in the waterfall.

While choice is certainly nice for providing a simple list of options, it does force the user into choosing one of those options. As a result, it’s not as easy to use choice when trying to guide the user with a list of options while also allowing them to type free-form, which is what you’ll want to do when the user first starts a session with the bot. In addition, you don’t get control over the interface provided.

Customizing the list of prompts

If you wish to customize the list of prompts, you need to set up a card. This can be an Adaptive Card, or one of the built-in cards such as thumbnail or hero. By using a card you can provide a bit more guidance to the channel on how you’d like your list of options to provide.

To allow the user to select from a list of options, you will add buttons to the card. Buttons can be set to either imBack, meaning the client will send the message back to the bot just as if the user typed it, or postBack, meaning the client will send the message to the bot without displaying it inside the client. Generally speaking, imBack is a better choice, as it makes it clear to the user something has happened, and can give the user a clue as to what to type in the future, should they so decide.

WARNING!!!

The code below is the wrong way to use buttons to provide a list of options, but it’s the most common mistake I see people make when using buttons with Bot Framework.

In the code snippet below, I want you to notice the addition of the buttons using builder.CardAction.imBack, and the call to session.send (where the mistake is).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(session, args, next) => {
const card = new builder.ThumbnailCard(session)
.text('Please choose from the list of colors')
.title('Colors')
.buttons([
new builder.CardAction.imBack(session, 'Red', 'Red'),
new builder.CardAction.imBack(session, 'Blue', 'Blue'),
new builder.CardAction.imBack(session, 'Green', 'Green'),
]);
const message = new builder.Message(session)
.addAttachment(card);
session.send(message);
},
(session, results, next) => {
if(results.response && results.response.entity)
session.endConversation(`You chose ${results.response.entity}`);
else
session.endConversation(`Sorry, I didn't understand your choice.`);
}

If you added this dialog to a bot and ran it, you’d see the following output:

Repeating buttons

The mistake, as I mentioned above, is at session.send. When using session.send in the middle of a waterfall dialog, the bot is left in a state where it’s not expecting the user to respond. As a result, when the user does respond by clicking on Blue, the bot simply returns back to the current step in the waterfall, and not to the next one. You can click the buttons as long as you’d like, and you’ll see them continuing to pop up.

The correct way to do it

In order for the bot to be in a state that expects user input and continues to the next step of a waterfall, you must use a prompt. When using buttons inside of a card, you can choose either a text or choice prompt. When using a text prompt, the bot can accept any input in addition to the buttons you provided. This can allow the user to be more free-form as needed. choice prompts, however, will limit the user to the list of choices, just as if you created it the traditional way mentioned earlier.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Using a choice prompt with custom buttons
// For simplicity, I removed the retry prompts, but you can continue to use them
// If you wanted to use a text prompt, you'd simply use:
// builder.Prompts.text(session, message);
(session, args, next) => {
const choices = ['Red', 'Blue', 'Green']
const card = new builder.ThumbnailCard(session)
.text('Please choose from the list of colors')
.title('Colors')
.buttons(choices.map(choice => new builder.CardAction.imBack(session, choice, choice)));
const message = new builder.Message(session)
.addAttachment(card);
builder.Prompts.choice(session, message, choices);
},
(session, results, next) => {
if(results.response && results.response.entity)
session.endConversation(`You chose ${results.response.entity}`);
else
session.endConversation(`Sorry, I didn't understand your choice.`);
}

Providing a menu

As I mentioned at the beginning of this post[1], one of the keys to a good user experience in a bot is to provide guidance to the user, otherwise you’re just giving them a C-prompt.Again, the easiest way to do this is via buttons.

We’ve already seen that imBack behaves just as if the user typed the value manually. We can take advantage of this fact by providing the list of options, and ensuring the values match the intents provided in the bot.

You’ll notice in the code sample below I created a bot with two simple dialogs, and the default dialog sends down the buttons inside of a card. By calling endConversation, the bot sends down the card and closes off the conversation. When the user clicks on a button it’s just as if the user typed in the value, and the bot will then route the request to the appropriate dialog. The user is free at this point to either click one of the provided buttons, or type in whatever command they desire.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const bot = new builder.UniversalBot(
new builder.ChatConnector({
appId: process.env.MICROSOFT_APP_ID,
appPassword: process.env.MICROSOFT_APP_PASSWORD
}),
(session) => {
const card = new builder.ThumbnailCard(session)
.title('Sample bot')
.text(`Hi there! I'm the sample bot.
You can choose one of the options below
or type in a command of your own
(assuming I support it)`)
.buttons([
builder.CardAction.imBack(session, 'Hello', 'Hello'),
builder.CardAction.imBack(session, 'Greetings', 'Greetings'),
]);
const message = new builder.Message(session)
.addAttachment(card);
session.endConversation(message);
}
);
bot.dialog('Hello', (session) => {
session.endConversation(`The Hello Dialog`)
}).triggerAction( { matches: /Hello/ } );
bot.dialog('Greetings', (session) => {
session.endConversation(`The Greetings Dialog`)
}).triggerAction( { matches: /Greetings/ } );

The updated bot now performs as displayed below. In the dialog I started by typing test to trigger the bot. I then clicked on Hello, which displayed the Hello Dialog message. I completed the exchange by typing Hello, which, as you see, sent the same Hello Dialog message.

Introduction with buttons

Conclusion

I’ve said it before, and I’ll certainly say it again - buttons exist for a reason. Buttons can help you provide a good UI/UX for users in any type of application, and bots are no exception. You can use buttons to both limit the amount of typing required, and to help guide the user’s experience with the bot.

[1] This exceedingly long post?

閱讀本文

2017-02-21
Managing conversations and dialogs in Microsoft Bot Framework using Node.JS

Communication with a user via a bot built with Microsoft Bot Framework is managed via conversations, dialogs, waterfalls, and steps. As the user interacts with the bot, the bot will start, stop, and switch between various dialogs in response to the messages the user sends. Knowing how to manage dialogs in Bot Framework is one of the keys to successfully designing and creating a bot.

Dialogs and conversations, defined

At its most basic level, a dialog is a reusable module, a collection of methods, which performs an operation, such as completing an action on the user’s behalf, or collecting information from the user. By creating dialogs you can add reuse to your bot, enable better communication with the user, and simplify what would otherwise be complex logic. Dialogs also contain state specific to the dialog in dialogData.

A conversation is a parent to dialogs, and contains the dialog stack. It also maintains two types of state, conversationData, shared between all users in the conversation, and privateConversationData, which is state data specific to that user.

Waterfalls

Every dialog you create will have a collection of one or more methods that will be executed in a waterfall pattern. As each method completes, the next one in the waterfall will be executed.

Dialog Stack

Your bot will maintain a stack of dialogs. The stack works just like a normal LIFO stack), meaning the last dialog added will be the first one completed, and when a dialog completes control will then return to the previous dialog.

Managing dialogs

Bots come in many shapes, sizes, and forms. Some bots are simply front ends to existing APIs, and respond to simple commands. Others are more complex, with back and forth messages between the user and bot, branching based on information collected from the user and the current state of the application. Depending on the requirements for the bot you’re building, you’ll need various tools at your disposal to start and stop dialogs.

Starting dialogs

Dialogs can be started in a few ways. Every bot has a default, sometimes called a root dialog, which is executed when no other dialog has been started, and no other ones have been triggered via other means. You can create a dialog that responds globally to certain commands by using triggerAction or beginDialogAction. triggerAction is registered globally to the bot, while beginDialogAction registers the command to just that dialog. Finally, you can programmatically start a dialog by calling either beginDialog or replaceDialog, which will allow you to add a dialog to the stack or replace the current dialog, respectively.

Ending dialogs and conversations

When a bot reaches the end of a waterfall, the next message will look for the next step in the waterfall. If there is no step, the bot simply doesn’t respond, naturally ending the conversation or dialog. This can provide a bit of a confusing experience for the user, as they may need to retype their message to get a response from the bot. It can also be confusing for the developer, as there may be many ways a dialog might end depending on the logic.

As a result, when a conversation or dialog has come to an end, it’s a best practice to explicitly call endConversation, endDialog, or endDialogWithResult. endConversation both clears the current dialog stack and resets all data stored in the session, except userData. Both endDialog and endDialogWithResult end the dialog, clear out dialogData, and control to previous dialog in the stack. Unlike endDialog, endDialogWithResult allows you to pass arguments into the previous dialog, which will be available in the second parameter of the first method in the waterfall (typically named results).

State management

Ending a conversation or dialog will also remove the associated state data. This is important to remember when deciding where to store state data. The best practices of minimizing scope of state data apply to bots, just as they do to any other application.

The place where state lifespan becomes trickiest is dialogData. If you start a new dialog, the dialog doesn’t receive the data from the calling dialog. In addition, when a dialog completes, the previous dialog doesn’t receive the data from the calling dialog. You can overcome this by using arguments. endDialogWithResult allows you to pass arguments to the prior dialog, while both beginDialog and replaceDialog allow you to pass arguments into the new dialog.

The sample application

The sample application we will be building through the next set of examples is a simple calculator bot. Our calculator bot will allow the user to enter numbers, and once they say total we’ll display the total and allow them to start all over again. We’ll also want to allow the user to get help at any time, and to cancel as needed. The sample code is provided on GitHub.

Default dialog

Starting with version 3.5 of Microsoft Bot Framework, the default or root dialog is registered as the second parameter in the constructor for UniversalBot. In prior versions, this was done by adding a dialog named /, which led to naming similar to that of URLs, which really isn’t appropriate when naming dialogs.

The default dialog is executed whenever the dialog stack is empty, and no other dialog is triggered via LUIS or another recognizer. (We’ll see how to register dialogs using triggerAction a little later.) As a result, the default dialog should provide some contextual information to the user, such as a list of available commands and an overview of what the bot can perform.

From a design perspective, don’t be afraid to send buttons to the user to help guide them through the experience; bots don’t need to be text only. Buttons are a wonderful interface, as they can make it very clear what options the user can choose from, and limit the possibility of the user making a mistake.

To get started, we’ll set up our default dialog to present the user with two buttons, add and help. For our first pass, we’ll simply echo the user’s selection; we’ll add additional dialogs in the next section. We’ll do this by setting up a two step waterfall, where the first step will prompt the user, and the second will end the conversation.

Default dialog sample code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const builder = require('botbuilder');
const connector = new builder.ChatConnector({
appId: process.env.MICROSOFT_APP_ID,
appPassword: process.env.MICROSOFT_APP_PASSWORD
});
const bot = new builder.UniversalBot(connector, [
(session, args, next) => {
const card = new builder.ThumbnailCard(session);
card.buttons([
new builder.CardAction(session).title('Add a number').value('Add').type('imBack'),
new builder.CardAction(session).title('Get help').value('Help').type('imBack'),
]).text(`What would you like to do?`);
const message = new builder.Message(session);
message.addAttachment(card);
session.send(`Hi there! I'm the calculator bot! I can add numbers for you.`);
const choices = ['Add', 'Help'];
builder.Prompts.choice(session, message, choices);
},
(session, results, next) => {
session.endConversation(`You chose ${results.response.entity}`);
},
]);

Sample code

Working with dialogs

One of the biggest challenges when creating a bot is dealing with the fact users can be random. Imagine the following exchange:

1
2
3
4
User: I'd like to make a reservation
Bot: Sure! How many people?
User: Do you have a vegan menu?
Bot: ???

This is a common scenario. The user sends a message to the bot. The bot responds. The user gets a new piece of information, in this case their friend is a vegan, and thus asks about a vegan menu. The bot is now stuck, because it wasn’t expecting that response. triggerAction allows you to register a global command of sorts with the bot, and ensure the appropriate dialog is executed for every request.

Naming dialogs

In prior versions of Bot Framework, developers typically started every dialog name with /. This was because when registering the default dialog in earlier versions you named it /. As you’ve already seen, that’s not the case starting with version 3.5. As a result, you give your dialog a name that appropriately describes the operation the dialog is built to perform.

Registering a dialog

bot.dialog is used to register a dialog. The two parameters you’ll provide are the name of the dialog, and the array of methods you wish to execute when the user enters the dialog. Let’s create the starter for add dialog. For now, we’ll leave it with the simple echo, and introduce new functionality as we go forward.

Dialog sample code

1
2
3
4
5
bot.dialog('AddNumber', [
(session, args, next) => {
session.endConversation(`This is the AddNumber dialog`);
},
]);

Using triggerAction to start a dialog

We want to register our AddNumber dialog with the bot so whenever the user types add this dialog will be executed. This is done through the use of triggerAction, which is a method available on Dialog. triggerAction accepts a parameter of type ITriggerActionOptions.

ITriggerActionOptions has a few properties, the most important of which is matches. Matches will either be a regular expression to match a string typed in by the user, such as add in our case, or a string literal if the match will be done through the use of a recognizer, such as one from LUIS.

Let’s update our bot to register AddNumber to be started when the user types add. We’ll remove the second step from the default dialog and take advantage of the behavior of our buttons, which will send the text of the button to the bot, much in the same way as if the user typed it themselves.

triggerAction sample code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// just the updated code
const bot = new builder.UniversalBot(connector, [
(session, args, next) => {
const card = new builder.ThumbnailCard(session);
card.buttons([
new builder.CardAction(session).title('Add a number').value('Add').type('imBack'),
new builder.CardAction(session).title('Get help').value('Help').type('imBack'),
]).text(`What would you like to do?`);
const message = new builder.Message(session);
message.addAttachment(card);
session.send(`Hi there! I'm the calculator bot! I can add numbers for you.`);
// we can end the conversation here
// the buttons will provide the appropriate message
session.endConversation(message);
},
]);
bot.dialog('AddNumber', [
(session, args, next) => {
session.endConversation(`This is the AddNumber dialog`);
},
]).triggerAction({matches: /^add$/i});

Sample code

triggerAction notes

triggerAction is a global registration of the command for the bot. If you wish to limit that to an individual dialog, use beginDialogAction, which we’ll discuss later.

Also, triggerAction replaces the entire current dialog stack with the new dialog. While that can be good for AddNumber, that wouldn’t be good for a dialog to provide help. We’ll see a little later how onSelectAction can be used to manage this behavior.

If you execute the bot at this point you’ll notice clicking Add on the buttons, or simply typing it, will cause the bot to send the message This is the AddNumber dialog. You’ll also notice that help, at present, does nothing. We’ll handle that in a bit.

Using replaceDialog to replace the current dialog

Let’s talk a little bit about our logic for AddNumber. We want to prompt the user for a number, add it to our running total, and then ask the user for the next number. Basically, we just need to restart the same dialog over and over again. We can use replaceDialog to perform this action.

In the first step of our waterfall, we’ll check to see if there is a running total available in privateConversationData, and create one if it doesn’t exist. We’ll then prompt the user for the number they want to add.

In the second step, we’ll retrieve the number, add it to our running total, and then start the dialog over again by calling replaceDialog.

replaceDialog sample code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bot.dialog('AddNumber', [
(session, args, next) => {
let message = null;
if(!session.privateConversationData.runningTotal) {
message = `Give me the first number.`;
session.privateConversationData.runningTotal = 0;
} else {
message = `Give me the next number, or say **total** to display the total.`;
}
builder.Prompts.number(session, message, {maxRetries: 3});
},
(session, results, next) => {
if(results.response) {
session.privateConversationData.runningTotal += results.response;
session.replaceDialog('AddNumber');
} else {
session.endConversation(`Sorry, I don't understand. Let's start over.`);
}
},
]).triggerAction({matches: /^add$/i});

Sample code

replaceDialog notes

replaceDialog takes two parameters, the first being the name of the dialog with which you wish to replace the current dialog, and the second being the arguments for the new dialog. The object you provide as the second parameter will be available in the first function in the new dialog’s waterfall in the second parameter (typically named args).

Using beginDialogAction to localize commands

It doesn’t make a lot of sense for our bot to have a global total command. After all, it’s only valid if we’re currently adding numbers. Using beginDialogAction allows you to register commands specific to that dialog, rather than global to the bot. By using beginDialogAction, we can ensure total is only executed when we’re in the process of running a total.

The syntax for beginDialogAction is similar to triggerAction. You provide the name of the DialogAction you’re creating, the name of the Dialog you wish to start, and the parameters for controlling when the dialog will be started.

beginDialogAction sample code

1
2
3
4
5
6
7
8
9
10
11
bot.dialog('AddNumber', [
// existing waterfall code
])
.triggerAction({matches: /^add$/i})
.beginDialogAction('Total', 'Total', { matches: /^total$/});
bot.dialog('Total', [
(session, results, next) => {
session.endConversation(`The total is ${session.privateConversationData.runningTotal}`);
},
]);

Sample code

beginDialogAction notes

By using endConversation, we reset the entire conversation back to its starting state. This will automatically clear out any privateConversationData, as the conversation has ended.

Using onSelectAction to control triggerAction behavior

By default, triggerAction will reset the current dialog stack with the new dialog. In the case of AddNumber that’s just fine; the logic on the dialog is designed for the dialog to continually restart. But this is problematic when it comes to Help. Needless to say, we don’t want to reset the entire set of dialogs when the user types help; we want to allow the user to pick up right where they left off.

Bot Framework provides beginDialog for adding a dialog to the stack. When that dialog completes, it returns to the control to the active step in the prior dialog. Or, in terms of the case of our Help example, it will allow the user to pick up where they left off.

The onSelectAction property on ITriggerActionOptions executes when the bot is about to start the dialog being triggered. By using this event, we can change the way the dialog is started, using beginDialog, which will add the dialog to the stack instead of replacing stack. The first parameter is the name of the dialog we wish to start, which is provided in args.action, and the second is the args parameter we want to pass into the the dialog when it starts. The code sample below will ensure we return control to the prior dialog when this one completes.

onSelectAction sample code

1
2
3
4
5
6
7
8
9
10
bot.dialog('Help', [
(session, args, next) => {
session.endDialog(`You can type **add** to add numbers.`);
}
]).triggerAction({
matches: /^help/i,
onSelectAction: (session, args) => {
session.beginDialog(args.action, args);
}
});

Sample code

onSelectAction notes

When using beginDialog, don’t hard code the name of the dialog you’re about to start, but rather use args.action. Otherwise, you’ll notice the dialog won’t actually start.

Using beginDialogAction to centralize help messaging

One of the challenges with the help solution we created earlier is it can only provide generic help; whenever the user types help the exact same message is sent to the user. By using beginDialogAction you can parameters to the triggered dialog, allowing you to centralize messaging for help. In our case, we’ll use the name of the current action as the key to the message we want to send.

beginDialogAction to centralize help sample code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
bot.dialog('AddNumber', [
// existing waterfall code snipped
])
.triggerAction({matches: /^add$/i})
.beginDialogAction('Total', 'Total', { matches: /^total$/})
.beginDialogAction('HelpAddNumber', 'Help', { matches: /^help$/, dialogArgs: {action: 'AddNumber'} });
bot.dialog('Total', [
(session, results, next) => {
session.endConversation(`The total is ${session.privateConversationData.runningTotal}`);
},
]);
bot.dialog('Help', [
(session, args, next) => {
let message = '';
switch(args.action) {
case 'AddNumber':
message = 'You can either type the next number, or use **total** to get the total.';
break;
default:
message = 'You can type **add** to add numbers.';
break;
}
session.endDialog(message);
}
]).triggerAction({
matches: /^help/i,
onSelectAction: (session, args) => {
session.beginDialog(args.action, args);
}
});

Sample code

Using cancelAction and endConversationAction

If you’ve made it to this point in the article, you already have the skills necessary to create a global cancel operation - you’d add a new dialog, register it with triggerAction, and add a string match for the word cancel. The dialog would then call endConversation with a friendly message, and the user would be able to restart he operation.

However, let’s say you wanted to provide granular support for cancel operations, changing the behavior on different dialogs, or maybe not allowing a cancel on a dialog at all. This is where cancelAction and endConversationAction come into place. Both are tied to a specific dialog, and cancelAction cancels just the dialog, while endConversationAction cancels the entire conversation.

The second parameter you’ll pass into cancelAction is ICancelActionOptions, which includes the matches and onSelectAction properties we’ve seen before. It also adds confirmPrompt, which, if set, will prompt the user if they actually want to cancel.

cancelAction sample code

1
2
3
4
5
6
7
bot.dialog('AddNumber', [
// prior code for AddNumber snipped for clarity
])
.endConversationAction('CancelAddNumber', 'Operation cancelled', {
matches: /^cancel$/,
confirmPrompt: `Are you sure you wish to cancel?`
})

Sample code

Conclusion

Bot Framework offers many options and methods for managing dialogs and responding to user requests. Harnessing the the power provided by dialogs allows you to create bots that can have conversations with your users that feel more natural.

Acknowledgements

Thank you to Nafis Zaman for the catch on the behavior of cancelAction.

閱讀本文

2017-02-17
New York Marathon - 2016

The New York Marathon. There’s really nothing else you need to say to runners and non-runners alike. It’s the largest marathon in the world, and arguably the most prestigious. While it doesn’t have the qualifying cache that Boston does, it’s a marathon everyone knows, and is on the bucket list of every runner, or at least all the ones I know.

As a Jersey Boy, it’s a race I’ve wanted to do for as long as I can remember, long before I laced up a pair of running shoes and heaved my way around Fiesta Island with a friend for my first “run”. I’d entered the lottery 3 times prior with no luck. So my joy upon seeing that email that contained the word “Congratulations” cannot even begin to be described. I’m honestly getting chills just sitting here thinking back to that day.

The Training

Anyone who knows me knows my snake-bitten history with marathon training. This cycle was no exception in that aspect, but there were a few other factors that contributed to a less-than-optimal summer.

For starters, and I’m just going to point the biggest finger at myself, I was frankly just tired. I’d done Grandma’s Marathon at the end of June. For those of you scoring at home (or even if you’re alone), that’s just 4 months before the New York Marathon, or about 20 weeks. That doesn’t give you much “I’m just going to sit on my keester and do nothing” time. The moment I finished my last marathon I was already back in training mode. That was a bit much for me, and I was burned out going into the next round. As such, I wasn’t as committed as I should have been, and it certainly showed.

In addition, my travel schedule was a struggle. While I used to travel full time, my schedule and destinations were relatively predictable, so it was easy to work my training into the week. During the training period I had a handful of oddball trips that threw off everything, including a trip to Japan. As a perfect example, I’d hoped to knock out an 18 miler in Japan - the weather conspired against me, and my body quit after 14. (Actually, it quit after 8, but I pushed through the rest.) While it did give me an opportunity to do 18 with a great friend the next week, it wasn’t where I needed to be.

The week after said 18 I’d intended to do “the 20 miler”. After about 3 miles I had a tendon behind my knee start to complain. I kept thinking it just needed to loosen up, but after I finished 6, and the group I was with got back to the parking lot where we were going to meet more people for the rest of the run, I knew I was done for the day. I tried to stretch, which elicited a stream of curse words that would make a sailor blush. I hopped in the car, had a good cry thinking I wouldn’t be able to run after all. I went to my PT, who threw everything he had at it, rested, compressed, and everything else, in hopes I was able to run.

Amazingly, I was able to meet the one true goal every marathoner has: toeing the starting line. And this time it was at the foot of the Verrazano.

New York City Logistics

I knew I wanted to stay in the Financial District (FiDi), because it’s both quiet at night, and walking distance to the Staten Island Ferry. I found a nice little Airbnb that was just a few blocks away from the terminal, and thought all was good. Until about 6 weeks before the race when I received an email from the host saying his building’s management wouldn’t let him rent the place out any longer. Anyone who knows Airbnb in New York knows how little the government cares for Airbnb, and how little Airbnb cares for government regulations. So despite Airbnb being one of the sponsors of the event, it was pretty clear to me this wasn’t going to be an option, or at least not a reliable one. (FWIW, I’d suggest avoiding Airbnb in New York for this exact reason.)

Fortunately I managed to get a great rate on the DoubleTree, which is about 3 blocks from the terminal. It’s also a hotel I’d stayed at many times, so I was familiar with both the hotel and the area around it. I’m all about the comfort provided by routine, and this was going to give me exactly that.

I arrived on the Thursday before the race, so I could see Tim Minchin perform, and to get adjusted to the time zone. Landed in Newark, Lyft (speaking of companies with contentious relationships with the government) up to FiDi, and focused on relaxing as much as possible.

The Expo

Obligatory Expo Photo

I’d been told many times to get to the expo as early in the day, and the week, as possible. Heed this advice! While they take over a convention center floor, and have an amazing amount of real estate, there’s still 50,000 runners that need to make their way through the area, not to mention family and friends they bring along for support.

I got there at about 10:45, 45 minutes after opening, and it was already very busy.

That said, it’s as well organized as an event of this size can be. If you were smart enough to print out your check-in sheet at home you could head straight over to pickup. Or, if you were like me, you head over to a little kiosk, and get a little receipt printout, and then go get your packet. From there it’s over to grab your shirt, where there was a huge line for Men’s Medium. Fortunately I still have a few extra pounds on my frame, so I was grabbing a Large, and was through that pretty quickly.

Next up - swag. Yeah, we’re going to ignore the race fee, and the free shirt they just gave me. I needed more swag. So through the swag store I went, picking up a jacket. And another shirt. And a pint glass. And a hat. (I’m honestly I was that restrained.)

From there it’s on to the main expo floor, where you’ll find vendors selling all manner of snake oil, running gear, and last minute supplies such as gels. I made a bee-line for the CEP section to find a quad sleeve to help my ailing hamstring tendon. Upon acquiring that, I checked out a great little seminar put on by the Whippets running group, who walked everyone through the course. If it’s your first New York Marathon, I can’t recommend attending this enough. As an added bonus, I happened to see someone with a custom bib with his name on it; he pointed me at the station that was doing that, and grabbed ones that said “Chris” and “Jersey”, unsure of which one I was going to wear on race day.

Finally I realized I was tired, and hungry, and needed to get out of there. I spent about 3 hours there, and I’d say that’s probably about average for most people.

The Day Before

My wife took the red-eye on Friday into New York, and my brother caught an early flight from Burlington down. My support crew had arrived. Just having them there gave me great comfort.

I truly wouldn't be able to do this without amazing support

We spent Saturday doing a dry run of the three different viewing spots they were going to cheer me on at. It worked well for them, as they got to see the locations, and which trains they needed. It worked well for me, as it gave me great visuals of where I was going to be on the course, and where to look for them.

We also walked about the last 2 miles of the race. While it was much longer than I really wanted to walk, I wanted to see the last mile. In the end, I’m glad that I walked the distance. It allowed me to make a few mental notes in regards to landmarks, and to see the exit and re-entry into Central Park.

It also allowed me to see the hill that is the .2 of the 26.2. Make sure you’re ready for that! They make you work for that medal.

BTW, if you’re looking for a good cheer strategy, take a train out to somewhere close to Barclay’s Center (it was the R train for us). You can see people at the 8 mile mark, just after where all the runners come together (more on that later). From there, hop the 4 train to somewhere along 1st. The crowds start to think towards the 100 blocks; my cheer crew waited for me on 86th. From there they can walk over to 5th for one last cheer. From 5th, you can catch one of the paths across the park around 86th and 5th. I was able to meet up with them on Columbus and 74th. It all worked out perfectly. Granted, you can’t be much faster than about a 4 hour time for the 3x strategy to work, so YMMV.

We bid my brother farewell for the night as he had family obligations (which I managed to dodge.) My wife and I went off to find Japanese food (rice is my pre-race carb of choice), and then off to sleep with visions of PRs dancing in my head.

The Morning

Now I should mention at this point that I, like many a runner, have a delicate stomach. Part of my plan behind paying the money for the DoubleTree in FiDi was to be in a familiar neighborhood, with familiar shops. During the short period of time we were there I’d asked the Essen deli/bagel shop about their hours. They assured me they were 24/7. Perfect!

I woke up after a pretty good night’s sleep, and began my preparations. I’d already laid out my deflated runner, so I knew what I was wearing, and that I had everything in regards to that. I put everything together, donning my running outfit, filling my water bottles, and tossing on my donation outfit of a sweatshirt, sweatpants and a robe.

That's a comfortable looking robe

Yes, a robe. I mean, if you’re going to be up that early, you may as well have a robe.

After kissing my wife goodbye, and getting the good luck wishes I certainly needed, I roamed over to Essen for my english muffin and peanut butter. Only, the “grill”, so to speak, wasn’t open. OK, deep breath. I’m not going to let this get into my head. So I bought two raw english muffins from the guy behind the counter, and a Kind Bar, which I hoped would be OK with my stomach.

I walked over to the ferry, with hundreds of other runners. If it didn’t already start to hit me that I was about to run the New York Marathon, it became a reality at that point. I chatted with a couple of other runners on the way over. Seeing the Staten Island Ferry sign elicited a couple of tears.

The security presence was obvious, but not overwhelming. My bags were sniffed by a couple of rather cute dogs, and away I went onto the 6:30 ferry. I was originally set for the 6:45 ferry, and I was hoping to see a couple of Seattle Green Lake Running Group (SGLRG) runners, but the draw of just getting to Staten Island was too much. (As it turned out, the ferries after 7:30 started having issues from what I’ve heard, so maybe it’s just as well.)

The ferry was full of runners, save for a few people that were riding it because it’s, well, the Staten Island Ferry, who weren’t necessarily thrilled we were there. I <3 NY. The ride is relatively quick, and gives you an amazing view of Lady Liberty, which is a wonderful way to start any morning.

Good morning Lady Liberty

Upon arriving on Staten Island it was a bus ride over to the start. Relax. That’s where the line really starts, as well as the waiting. Bring a paper, a copy of Runner’s World, or something to pass the time. Or, just people watch. Between the people who’ve done numerous marathons, to the first timers, to everyone else, there’s plenty of sights to see. Take it all in. You’re about to run the greatest marathon on the planet (again - sorry, Boston).

We unloaded at Fort Wadsworth, where we were greeted by more security, and more dogs. And then it was time to get prepped for the race.

The Waiting

Follow the signs

I’d read a few things in the past that said to get to Fort Wadsworth as late as possible. I landed a good 2 hours before the start, and I felt like that was perfect. Next time I run the race I fully intend on getting over to Staten Island relatively early.

The organizers have this down to a science. They know exactly what they’re doing. The race is broken down into 4 separate start times, and then 3 different colors, and then corrals from there. On top of that, the start area has a ton of real estate on which to spread out. As a result, it oddly feels like a much smaller race than it actually is. There was plenty of space to spread out, to take care of last minute preparations, or to just close your eyes and relax on the grass.

That said, the porta-potty lines are ridiculous. Make sure you give yourself plenty of time to answer nature’s call. In fact, it’s not a terrible idea to take care of things an hop back in line, just in case.

As for me, I worked on my last bits of prep. I assembled the rest of my outfit, attaching my bib and my name bib to my shirt. I drank more and more water. I worked on not letting the fact I was about to run the New York Marathon hit me, with mixed results. Seeing the Verazano Narrows Bridge in the distance is hard to ignore.

The Verazzano

The Time Goal

At some point I should probably talk about my time goal. Every runner has one, despite how much they might deny it. If they are admitting to a time goal, they probably have a faster one they’re not really not wanting to make public.

I’m not that runner. I have one simple marathon goal: I want the first number to be a 3. I don’t care if it’s followed by 59:59, my white unicorn starts with the number 3. Just once I want to finish under four hours.

Considering the disjointed training plan I had I wasn’t sure what my body might offer. But during that “taper” period, where I was mostly just trying to not upset that tendon, I was running a comfortable 8:40 pace, or about 30 seconds faster than what I’d need on race day. In fact, the last run I had with my Canadian Running Wife (CRW) featured a push up a hill which made her work, and she’s much faster than I am.

After all of that, I thought I’d be able to finally find that white unicorn.

The Corrals and Colors

The race of course starts on the Verazano Narrows Bridge. There’s two stories on this bridge. And a couple of entrance/exit ramps on the other side in Brooklyn. And, of course, 50,000 humans to try and work with.

As a result, they break things down into waves, colors, and corrals. The corrals are mostly what you’d expect - packs of runners. And the waves are the various start times. Where things are truly different than most races are the colors. There are actually three paths for the first few miles of the race. Two colors, blue and green, take the top deck of the bridge, while orange takes the lower deck. Each of the three colors exits on a different ramp in Brooklyn. Because they’re all at different paces, there’s enough time, and distance, for everyone to naturally spread out before hitting the point in Brooklyn where everyone is brought together. Don’t get me wrong, there will always be people around you, but you’ll never feel like you’re fighting for elbow room.

Corral A

My Start

They called my (now updated) corral of Wave 3, Blue, Corral A. I roamed over and waited. And waited. And waited. They were still unloading wave 2, which took quite a while. I used the time for last minute prep, ditching my robe (sadly). I dropped a Nuun into my water belt bottles, and filled them with water. And I started to get a feel for the weather.

There are few things runners obsess over more than the weather, save for maybe pre-race-porta-potties. You could not have asked for a better day for a marathon. It was in the 50s to start, and sunny. No threat of rain, but there was talk of a little bit of wind, which did hit at times. But really, gorgeous running weather.

They opened the corral, which is really just another brief waiting area before you walk to the start. I saw a glimpse of the 4:00 pacer, who was towards the front of the corral, as I waited in line to, well, take care of business. By the time I got out they were long since gone.

I walked towards the start line with everyone as they released us, working on breathing exercises. Someone sang God Bless America, rather than the anthem, which got my blood going. I kept trying to find that 4:00 sign, wanting a pack to run with. Alas, I wasn’t able to find them. I ditched the sweatsuit and focused on the goal.

Then the cannon went off.

The Verazano Narrows Bridge

Marathon rules:

  1. Don’t go out too fast
  2. See Rule 1

It seems so simple. Especially in New York where the first mile is straight uphill. I mean, really, go slow. In theory, that first mile for someone trying to break 4:00 should still be around 10:00, if not even slower.

But, there’s a cannon. And cameras. And the fact that it’s the New York Marathon.

The adrenaline carried me up the hill in 9:40. I didn’t mean to run that fast, but there I was at the top of the bridge. Amazingly I still couldn’t find that 4:00 pacer, but at that point my concerns were focused around that lovely quad compression sleeve I’d purchased which was now around my knee.

Compression had been helping my tendon leading up to the race, so I was really hoping to have the sleeve for the race. The sleeve, however, had other plans. I’m no doctor, but I’m pretty sure having all of that around one’s knee is not a good thing. Once we hit the top of the bridge I stopped for a few seconds to pull the sleeve off and ditch it.

Then I focused my attention on relaxing. It’s just mile 2. I need to slow down. 9:40 was not where I wanted to be that first mile, so let me settle in and just enjoy the downhill.

The watch beeped at mile 2 at 8:20. So much for relaxing.

That's me on the lower right in green!!

Welcome to Brooklyn

I’d always been told that the New York Marathon was like no other marathon in regards to fans, that there would be fans the entire race. Obviously there are no fans on the Verazano, but landing in Brooklyn brought the first pack of fans.

It oddly felt at that point like a lot of other early marathon stages. Having run the San Diego Rock-n-Roll, it felt like I’d turned onto Washington St. There were people lining the streets, but only about one deep. And there were bands.

But there was still a different energy. There was a crescendo building.

I tried to settle in. Tried. I wanted nothing more than to just settle into a 9:09 pace (4:00 hours). But my legs just refused to go that slow. I was caught up in the energy. The fans and other runners carried me.

The crowds continued to build. Even though I was running in the middle of the street to try to just get into my own head and find my pace, I could still feel the energy they were giving me.

I crossed the 10K mark a full minute ahead of schedule. This was not good. And I knew it wasn’t good. My body started to feel it. The four to six mile section of the course features a steady downhill, which beat up my quads. But I kept hoping my legs would come back to me, and I knew that I had my cheering squad at mile 8.

I focused on the couple of turns that took us through the heart of Brooklyn and towards Abram and Karin. I took the right, drifted towards the left, and saw the green and pink poster boards they had. Giving them both high fives filled me with more energy than I can explain.

The crowds through that area are amazing. They’re 4 to 5 deep. And screaming at the top of their lungs. You feel like an absolute rock star. You’re on top of the world.

Around mile 10 you hit the traditional Jewish part of town. There are still fans there, but it’s more quiet. People are just going on about their day, mostly just ambivalent or annoyed at your presence. It was surreal coming out of such an energy filled section of the course to the exact opposite, and get a glimpse of the town going on about its day.

As for me, well, that was when I started spewing axle grease all over the course. My quads started to give way. As did almost every other subsystem in my body. I felt lightheaded. And nauseated. And miserable. I started walking, weaving a little side to side. While it’s certainly hard to self-diagnose, I’d be willing to bet I was over-hydrated. Whatever it was, I just refused to let it stop me. Slow me down, sure. But I wasn’t going to stop.

I focused, took a deep breath, found some form of a cadence, and kept moving.

Welcome to Queens

You don’t spend much time in Queens. From a marathoner’s perspective, about the only thing you do in Queens is get ready for the bridge. You do have to climb the Pulaski Bridge to get into Queens, but you’re only in the borough long enough to make a few turns.

I did have to walk a bit in Queens. My motivation was still high, and I still had a goal. At this point I knew my white unicorn was gone, but I was still hoping for about a 4:15.

Welcome to Manhattan

OK, maybe not yet.

Now, full disclaimer, I’m generally not one to swear. Because there’s only one way to tell this story, and that’s as follows.

We took the left from Queens onto the Queensboro Fucking Bridge. I’m convinced that’s its real name. The Queensboro Fucking Bridge (QFB).

At this point, my will to live was slowly sucked away.

The QFB has many terrible features.

For starters, there are no fans. There’s no way onto the QFB unless you’re a runner. It’s just you, and the sounds of everyone else around you.

In addition, you’re on the lower deck. That, for me, and many others I’ve talked to, creates this terrible illusion the crest of the hill is just ahead, but it’s not. It seems to just go on forever.

The views are certainly amazing.

But the rest of the experience is disheartening.

I finally got to the end of this interminable bridge, and my quads were truly gone. I had to stop as I got onto 59th to stretch, something I’d never had to do in any marathon prior to New York.

Welcome to Manhattan (for reals)

If you’ve read anything about the New York Marathon, you’ve certainly heard about the crowds that await you at the end of the QFB. All of that is real, and it continues for a good couple of miles. Fans will be there 4 to 5 deep. There are truly no words in my vernacular to describe how amazing the atmosphere is through this section of the course.

When you finally take the left onto First you are greeted by unmatched energy, and a view of a sea of runners stretching out as far as one can see. It’s breathtaking. It truly feels at this point like you’re running the New York Marathon.

As for me, it’s now truly a struggle. My goal has been adjusted to just making the New York Times, who does a special section for the marathon listing off marathoners; I had heard the cutoff is 4:30.

Walk as needed, force myself to run as much as possible. But just keep moving.

I knew Karin and Abram were at 87th, and that was all I was focused on. I saw them, and gave them both a hug. Normally I wouldn’t have done that, but considering most of my time goals were shot, I wanted to take the time to thank them.

I shared my hatred for the QFB with them, and said I was happy I wasn’t the guy I passed a little earlier who’d pooped himself. It’s all about perspective.

From there I kept working my way on to the Bronx, with a single focus - seeing Karin and Abram one more time. I’m not going to say I would have dropped out of the race, but at this point I was rather miserable, and my main reason for staying on the course was to see them one last time on 5th.

Welcome to The Bronx

As much as I loathe the QFB, and I do, I have to say that the bridge leading into The Bronx has its own special kind of awful. It’s cambered, and while not long is just steep enough to truly frustrate you. Or, at least frustrate me.

That said, the fans in The Bronx, while certainly not as numerous as Manhattan, are feverish. They were truly proud of their neighborhood, and wanted you to know it. I appreciated that more than my face showed.

Although, my face only showed misery at that point.

Welcome Back to Manhattan

At this point my body is just shot. Mentally, I’m still in the game. After all, I’m running the New York Marathon. I mean, what could be better than that? But I couldn’t get much behind a 50/50 run/walk, and even a 50/50 ratio was a struggle at best. My goal had shifted to just finishing in under five hours.

After climbing the last bridge (finally!!) into Manhattan, I tried to just enjoy the atmosphere. The atmosphere through Harlem was all that I had hoped it would be. There was a church choir out singing, and, again, a prideful neighborhood.

It’s at this point I realize how well the course shows off the city and its neighborhoods. You get a great feel for what makes New York the greatest city on the planet.

It’s also when I realize the hill that is mile 23, and 5th Avenue.

As I mentioned earlier, one of the driving forces I had was to see Karin and Abram. We had made plans for them to be in the mid-to-upper 90s along 5th, on the left side. After taking the quick right, left, left, and right around the park, it was all I was focused on.

The hill was tougher than I expected. That said, the crowds were beyond comprehension.

Callback

At this point I’d like to double back to the name tag debate I had in my head before the race of Chris vs Jersey. While I truly hate being called Chris, I was worried that wearing “Jersey” in New York would bring me nothing but heckling. I asked a friend of mine who’d run the race a couple of years prior, Elaine, who told me there is no negativity on race day.

I can safely say she’s right. I heard nothing but either “Jersey Strong!!” or people chanting “Jersey”.

And down 5th, that energy kept me going.

Back to 5th

I hung to the left side where we’d agreed to meet, and didn’t see them. While I was disappointed, I kept to the left hoping they’d simply not ventured that far north, while accepting the fact I’d missed them.

And then there they were. I gave them both a hug.

Welcome to Central Park

Welcome to Central Park

Shortly after seeing Karin and Abram I turned right into Central Park. I’d seen the signs the day before, but seeing them on race day brought me to tears.

I love Central Park.

It is my favorite place on the planet to run, full stop.

Seeing that sign not only meant I had less than 3 miles to go, it also brought me into my “running mecca”.

It had been a while since I’ve run Central Park, so the walk the day before helped remind me that the path back towards Central Park South is longer than it seems. I focused on the sights, but also on hitting the 40K mark at 4:45. I knew, if nothing else, that I could force myself through 2K in under 15 minutes.

Coming down 5th, and then into Central Park, you’re just surrounded by runners and energy. It’s near deafening. It’s truly special.

The Last Point 2

Mile 26

Taking the right from Columbus Circle back into the park was one of the best feelings I’ve ever had. I saw the 26 Mile sign and started bawling. After a few steps I realized it’s hard to breathe while crying and managed to contain myself.

Then you hit the uphill that makes up that last 385 yards. It’s tough.

And then the finish. No words can describe finishing a marathon, and nowhere is that more true than in New York.

More tears. More joy. I just finished the New York Marathon.

The Next Mile

Yes, you read that right. There’s one more mile to go as you work your way out of the finishing chute. There’s 50,000 runners to contend with. As large as Central Park is, they still need to go somewhere. Add to that the fact that there’s only certain exit spots from the Park to the City, and you’re looking at a solid mile walk to the finish if you didn’t gear check (which I didn’t.) On top of it all, you have to walk uphill.

The Reunion

After making it through the next mile I met up with Abram and Karin at 75th & Columbus. Karin gave me a huge hug, and Abram handed me a can of Heady Topper. We figured on race day open container laws would be overlooked. :-)

It was then time to go find food and celebrate.

We walked into a German beer hall (Reichenbach Hall), where they cheered every runner who stumbled in. This city embraces the marathon.

And the celebration was in full swing.

Celebration

Post mortem

When it comes time to do it again (because there will be another go):

  • Focus on training
  • Focus on hills - lots of hills
  • Respect the course
  • Go out slower
  • Enjoy it all over again!

The reward

Race Day Tips

  • Wear your name on your shirt. Yes, you’ll feel a bit dorky, but I’m here to tell you that hearing your name chanted at mile 23 makes all the difference in the world.
  • If you’re going to have a cheer crew, work out exactly where they’re going to be. There are a lot of runners, and it’s easier for the runner to spot the fan. Work out the side, the corner, what they’re going to be wearing, what signs they’ll be holding, etc.
  • There are porta-potties where the busses pick up at the Staten Island Ferry. If you don’t feel the need to have an actual restroom, there are no lines there. It’s a great place to, well, take care of business.
  • Do not check a bag if at all possible. Having to walk through the bag check area is a challenge you don’t want to face when you finish.
  • Bring a phone if you can for post-race coordination. Having a Metro card and a $20 is also a good idea.
  • Enjoy it. It’s the New York Marathon.
閱讀本文

2016-12-14
Simple Bot Creation with QnA Maker

Note: this blog assumes you have used Azure to create services in the past

The problem

One of the the most compelling scenarios for a bot is to add it to Facebook. A Facebook page is rather static. Finding information about a business on a Facebook page can be a bit of a challenge. And while users can comment, or send a message, the only replies they’ll ever receive is from a human, meaning the owner of the small business needs to monitor Facebook.

Of course, if it’s a small business that the page is representing, there’s a good chance the business doesn’t have the resources to create a bot on their own. Or, even if the business is of a size where they have access to developers, the developers aren’t the domain experts - that’s the salespeople, managers, or other coworkers.

To make a long story short, developers are often required to create the bot, and build the knowledge base the bot will be using to provide answers. This is not an ideal situation.

The solution

Enter QnA Maker.

QnA Maker is a service that can look at an existing, structured, FAQ document, and extract a set of question and answers into a knowledge base. The knowledge base is editable through an interface designed for information workers, and is exposed via an easy to call REST endpoint for developers.

Getting started

To get started with QnA Maker, head on over to https://qnamaker.ai/. You can create a new service by clicking on New Service. From there, you’ll be able to give your service a name, and point to one or more FAQ pages on the web, or a document - Word, PDF, etc. - containing the questions and answers. After clicking create, the service will do its thing, creating a knowledge base that can be accessed via the endpoint.

Create new service

Managing the knowledge base

The knowledge base is a set of questions and answers. After creating it, you can manage it much in the same way you edit a spreadsheet. You can add new pairs by clicking on Add new QnA pair. You can also edit existing pairs in the table directly. Finally, if you wish to add a new question to an existing answer, you can hover over the question on the left side, click the ellipsis, and choose Add alternate phrasing.

One important thing to note about the knowledge base, is each question and answer is an individual entity; there is no parent/child relationship between multiple questions and a single answer. As a result, if you need to provide additional ways to ask a particular question with the same answer, you will need to have multiple copies of the same answer.

Managing the knowledge base

Testing and further tweaking the knowledge base

Once you’re happy with the first version of your knowledge base, click Save and retrain to ensure it’s up to date. Then, click Test on the left bar, which will present you with a familiar bot interface. From this interface, you can start testing your bot by typing in questions and seeing the various answers.

You’re also able to update the knowledge base from this interface. For example, if you type a question that’s a little ambiguous, the interface will show you multiple answers on the left side. You can simply click the answer you like the most to update the knowledge base to use that answer for the question you provided.

In addition, after asking a question, and being provided an answer, you can add additional phrasings of the same question on the right side.

Testing and tweaking the knowledge base

Some design notes

First and foremost, remember the eventual user experience for this knowledge base is via a bot. Bots should typically have personality, so don’t be afraid to modify some of the answers from their original form to make it read a bit more like a human typed it out, rather than a straight statement of facts. In addition, make sure you add multiple questions related to hello, hi, help, etc., to introduce your bot and help guide your user to understand the types of questions your knowledge base can answer. Finally, remember that while a single form of a question works well on a FAQ page, users can type the same question in multiple forms. It’s not a bad idea to ask other people to test your knowledge base to ensure you’re able to answer the same question in multiple forms.

And, once you’re ready to make the service available to a bot, click Save and retrain, and then Publish.

Using the knowledge base in a bot

QnA Maker exposes your knowledge base as a simple REST endpoint. You can access it via POST, passing a JSON object with a single property of question. The reply will be a JSON object with two properties - answer, which contains the answer, and score, which is a 0-100 integer of how sure the service is it has the right answer. In fact, you can use this endpoint in non-bot services as well.

Of course, the goal of this blog post is to show how you can deploy this without writing code. To achieve that goal, we’re going to use Azure Bot Services, which is built on top of Azure Functions. Azure Bot Services contains a set of prebuilt templates, including one for QnA Maker.

In the Azure Portal, click New, and then search for Bot Service (preview). The Azure Portal will walk you through creating the website and resource group. After it’s created, and you open the service, you will be prompted to create a bot in Bot Framework. This requires both an ID and a key, which you’ll create by clicking on Create Microsoft App ID and Password.

IMPORTANT: Make sure you copy the password after it’s created; it’s not displayed again! When you click on Finish and go back to Bot Framework, the ID will be copied automatically, but the key will not.

Once you’ve entered the ID and key, you can choose the language (C# or NodeJS), and then the template. The template you’ll want is Question and Answer. When you click Create bot, you’ll be prompted to select your knowledge base (or create a new one).

And you’re done!

And that’s it! Your bot is now on the Bot Framework, ready for testing, to be added to Skype, Facebook, etc. You now have a bot that can answer questions about your company, without having to write a single bit of code. In addition, you’ll be able to allow the domain experts update the knowledge base without any need for code updates - simply save and retrain, then publish, and your bot is updated.

A couple of last thoughts

While the focus has been on a no-code solution, you are absolutely free to incorporate a QnA Maker knowledge base into an existing bot, or to update the bot you just created to add your own custom code. And if you’re looking for somewhere to get started on creating bots, check out the Bots posts on this very blog, or the MVA I created with Ryan Volum.

閱讀本文

2016-11-15
Providing help through DialogAction

One of the greatest advantages of the bot interface is it allows the user to type effectively whatever it is they want.

One of the greatest challenges of the bot interface is it allows the user to type effectively whatever it is they want.

We need to guide the user, and to make it easy for them to figure out what commands are available, and what information they’re able to send to the bot. There are a few ways that we can assist the user, including providing buttons and choices. But sometimes it’s just as easy as allowing the user to type help.

Adding a global commands

If you’re going to add a help command, you need to make sure the user can type it wherever they are, and trigger the block of code to inform the user what is available to them. Bot Framework allows you to do this by creating a DialogAction. But before we get into creating a DialogAction, let’s discuss the concept of dialogs and conversations in a bot.

Dialogs and conversations

Bots contain a hierarchy of conversations and dialogs, which you get to define.

A dialog is a collection of messages back and forth between the user and the bot to collect information and perform an action on their behalf. A dialog might be the appropriate messages to obtain the type of service the user is interested in, determine which location the user is referring to when asking for store information, or the time the user wants to make a reservation for.

A conversation is a collection of dialogs. The conversation might use a dialog to walk through the steps listed above - service type, location and time - to complete the process of creating an appointment. By using dialogs, you can simplify the bot’s code, and enable reuse.

We will talk more in future blog posts about how to manage dialogs, but for right now this will enable us to create a DialogAction.

What is a DialogAction?

At the end of the day a DialogAction is a global way of starting a dialog. Unlike a traditional dialog, where it will be started or stopped based on a flow you define, a DialogAction is started based on the user typing in a particular keyword, regardless of where in the flow the user currently is. DialogActions are perfect for adding commands such as help, cancel or representative.

Creating a DialogAction

You register a DialogAction by using the bot function beginDialogAction. beginDialogAction accepts three parameters, a name for the DialogAction, the name of the Dialog you wish to start, and a named parameter with the regular expression the bot should look for when starting the dialog.

1
2
3
4
5
6
7
8
9
bot.beginDialogAction('help', '/help', { matches: /^help/ });
bot.dialog('/help', [
(session) => {
// whatever you need the dialog to do,
// such as sending a list of available commands
session.endDialog('in help');
}
]);

The first line registers a DialogAction named help, calling a Dialog named help. The DialogAction will be launched when the user types anything that begins with the word help.

The next line registers a dialog, named help. This dialog is just like a normal dialog. You could prompt the user at this point for additional information about what they might like, query the message property from session to determine the full text of what the user typed in order to provide more specific help.

DialogAction flow

The next question is what happens when the help Dialog (what it’s called in our case) completes. When endDialog is called, where in the flow will the user be dropped? As it turns out, they’ll pick up right where they left off.

Imagine if we had the following bot:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const builder = require('botbuilder');
const connector = new builder.ConsoleConnector();
const bot = new builder.UniversalBot(connector);
const dialog = new builder.IntentDialog()
.matches(/^load$/i, [
(session) => {
builder.Prompts.text(session, 'Please enter the name');
},
(session, results) => {
session.endConversation(`You're looking for ${results.response}`);
}
])
.onDefault((session) => {
session.endConversation(`Hi there! I'm a GitHub bot. I can load user profile information if you send the command **load**.`);
});
bot.dialog('/', dialog);
bot.beginDialogAction('help', '/help', { matches: /^help/ });
bot.dialog('/help', [
(session) => {
session.endDialog('This bot allows you to load GitHub data.');
}
]);
connector.listen();

Notice we have have an IntentDialog built with a load “command”. This kicks of a simple waterfall dialog which will prompt the user for the name of the user they wish to load, and then echos it back. If you ran the bot, and sent the commands load, followed by help, you’d see the following flow:

1
2
3
4
5
6
7
8
9
User: load
Bot: Please enter the name
User: help
Bot: This bot allows you to load GitHub data.
Bot: Please enter the name
User: GeekTrainer
Bot: You're looking for GeekTrainer

Notice that after the help dialog completes the user is again prompted to enter the name, picking right up where you left off. This simplifies the injection of the global help command, as you don’t need to code in where the user left, and then returned. The Bot Framework handles that for you.

Summary

One of the biggest issues in creating a flow with a chat bot is the fact a user can say nearly anything, or could potentially get lost and not know what messages the bot is looking to receive. A DialogAction allows you to add global commands, such as help or cancel, which can create a more elegant flow to the dialog.

閱讀本文

2016-10-24
Determining Intent Using Dialogs

What did you say?

Bots give you the ability to allow users to interact with your app through communication. As a result, figuring out what the user is trying to say, or their intent, is core to all bots you write. There are numerous ways to do this, including regular expressions and external recognizers such as LUIS.

For purposes of this blog post, we’re going to focus our attention on regular expressions. This will give us the ability to focus on design and dialogs without having to worry about training an external service. Don’t worry, though, we’ll absolutely see how to use LUIS, just not in this post.

Dialogs

In Bot Framework, a dialog is the core component to interacting with a user. A dialog is a set of back and forth messages between your bot and the user. In this back and forth you’ll figure out what the user is trying to accomplish, and collect the necessary information to complete the operation on their behalf.

Every dialog you create will have a match. The match will kick off the set of questions you’ll ask the user, and start the user down the process of fulfilling their request.

As mentioned above, there are two ways to “match” or determine the user’s intent, regular expressions or LUIS. Regular expressions are perfect for bots that respond to explicit commands such as create, stop or load. They’re also a great way to offer the user help.

Design Note

One big thing to keep in mind when designing a bot is no natural language processor is perfect. When people create their first bot, the most common mistake is to allow the user to type almost anything. The challenge is this is almost guaranteed to frustrate the user, and lead to more complex code trying to detect the user’s intent, only to misunderstand a higher percentage of statements.

Generally speaking, you want to guide the user as much as possible, and encourage them to issue terse commands. Not only will this make it easier for your bot to understand what the user is trying to tell it, it actually makes it easier for the user.

Think about a mobile phone, which is one of the most common bot clients. Typing on a small keyboard is a challenge at best, and the user isn’t going to type “I would like to find the profile GeekTrainer” or the like. By using terse commands and utterances, you’ll not only increase the percentage of statements you understand without clarification, you’ll make it easier for the user to interact with your bot. That’s a win/win.

In turn, make it easy for your user to understand what commands are available. By guiding the user through a set of questions, in an almost wizard-like pattern, you’ll increase the chances of success.

Creating dialogs

To determine the user’s intent by using regular expressions or other external recognizers, you use the IntentDialog. IntentDialog effectively has a set of events exposed via matches which allow you to execute at least one function in response to the detected event.

Let’s say you wanted to respond to the user’s command of “load”, and send a message in response. You could create a dialog by using the following code:

1
2
3
4
5
// snippet
let dialog = new builder.IntentDialog()
.matches(/load/i, (session) => {
session.send('Load message detected.');
});

matches takes two parameters - a regular expression which will be used to match the message sent by the user, and the function (or array of functions) to be called should there be a match. The function, or event handler if you will, takes three parameters, session, which we saw previously, args, which contains any additional information sent to the function, and next, which can be used to call the next function should we provide more than one in an array. For the moment, the only one that’s important, and the only one we’ve used thus far, is session.

To use this with a bot, you’ll create it and add the dialog like we did previously, only adding in the dialog object rather than a function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// text.js
// full code
const builder = require('botbuilder');
const connector = new builder.ConsoleConnector();
const bot = new builder.UniversalBot(connector);
const dialog = new builder.IntentDialog()
.matches(/load/i, (session) => {
session.send('Load message detected.');
});
bot.dialog('/', dialog);
connector.listen();

If you run the code, and send the word load, you’ll notice it sends the expected message.

1
2
3
node text.js
load
// output: Load message detected

Handling default

Over time you’ll add more intents. However, as we mentioned earlier, we want to make sure we are able to give the user a bit of guidance, especially if they send a message that we don’t understand at all. Dialogs support this through onDefault. onDefault, as you might suspect, executes as the default message when no matches are found. onDefault works just like any other handler, accepting one or more functions to execute in response to the user’s intent.

1
2
3
4
5
6
7
8
9
10
11
// existing code
const dialog = new builder.IntentDialog()
.matches(/load/i, (session) => {
session.send('Load message detected.');
})
.onDefault((session) => {
session.endConversation(`Hi there! I'm a GitHub bot. I can load user profile information if you send the command **load**.`);
});
// existing code

You’ll notice you don’t give onDefault a name because it’s of course also a name. You’ll also notice we used session.endConversation to send the message. endConversation ends the conversation, and the next message starts from the very top. In the case of our help message this is the perfect behavior. We’ve given the user the list of everything they can do. The next message they send, in theory anyway, will be one of those commands, and we’ll want to process it. The easiest way to handle it is to use the existing infrastructure we just created.

If you test the bot you just created, you should see the following:

1
2
3
node text.js
Hello
// output: Hi there! I'm a GitHub bot. I can load user profile information if you send the command load.

Summary

When creating a bot, the first thing you’ll do is determine what the user’s intent is; what are they trying to accomplish? This is done in a standard app by the user clicking on a button. Obviously, there are no buttons. When you get down to the basics, a bot is a text based application. Dialogs can make it easier to determine the user’s intent.

閱讀本文