Topical is a framework for modeling conversations in Microsoft BotBuilder 4.x using the Topics pattern.
Topical is an experimental framework, not a supported Microsoft product. Use at your own risk.
Like BotBuilder 4.x, Topical is in rapid development, and every aspect is subject to change in upcoming versions.
npm install botbuilder-topical
The Topics pattern models conversations as a dynamic tree of independent conversational topics, each with its own state. Activities pass down through the tree, each topic handling the activity and/or passing it on to a child topic as it sees fit. A child topic notifies its parent when it's done.
Note: the method names on this graphic need to be updated: init
-> onStart
and onReceive
-> onDispatch
The Topical library provides low-level support for this pattern.
The built-in botbuilder-dialogs
library takes the v3.x Node "Dialogs" pattern and improves upon it in two ways:
- you can choose to write Dialog logic as either a waterfall or a standard message loop
- you invoke the dialog stack from your main message loop, which means the confusing triggers/scorables concepts from v3 have been eliminated
However, the pattern is the same as v3.x: the current activity is routed directly to the current dialog (the one at the top of the stack). This means that each dialog doesn't have a chance to make decisions about how to respond to an activity, including whether to dispatch it to a child. That, and the fact that you are limited to a stack of dialogs (instead of a tree), means that you are very limited in the types of conversational flow that you can implement.
The Topics pattern was designed to solve these problems.
Also, many find Topical's API simpler and easier to use.
Here's a snippet that shows a common pattern: a "root" topic creating a child, dispatching messages to it, and, when the child completes, handling its return.
class Root extends Topic {
async onStart() {
await this.context.sendActivity("How can I help you today?");
}
async onDispatch() {
if (await this.dispatchToChild())
return;
if (this.context.activity.text === "book travel") {
await this.startChild(TravelTopic);
}
}
async onChildEnd() {
await this.context.sendActivity(`Welcome back to the Root!`);
}
}
A given topic can have:
- no children
- a single child
- multiple children
It's up to you, but the idea is that each Topic maps to a specific topic of conversation (thus the name).
For instance, the Travel topic could handle general questions about travel, but when the user is ready to book a flight it would spin up a child Flight topic, and start dispatching incoming messages to that child. Furthermore, when no airport has been defined, Flight spins up the Airport Picker topic and dispatches messages to it.
Topics can be written independently of one another and composed together.
An important detail is that delegation doesn't have to be all or nothing -- Travel could continue answering specific questions it recognizes, e.g. "Is it safe to travel to Ohio?", and pass the rest on to Flight. This is why each message travels down from the top of the topic tree.
Midway through booking a flight, as illustrated above, a user might want to look into hotels. Travel could recognize that question and spin up a Hotel topic. It could end the Flight topic, or keep them both active. How does Travel know where to send subsequent messages? That is the hard part. Topical provides the structure, you provide the logic.
Please do! SimpleForm is a (simple) example of a "form fill" Topic
that could be of general use (as in the alarm bot sample). It also demonstrates how to express a dependency on another Topic
(TextPrompt
).
First, initialize Topical by calling Topic.init
with a state storage provider. This is where Topical will persist each topic's state.
Topic.init(new MemoryStorage());
Then, create a subclass of Topic
which will be your "root". Every activity for every user in every conversation will flow through this topic. A typical root topic will create one or more children and dispatch messages to them. Every topic must be registered after it is defined:
class YourRootTopic extends Topic {
// your topic here
}
YourRootTopic.register();
Finally it's time to hook your root topic up to your message loop:
yourMessageLoop(async context => {
if (context.activity.type === 'conversationUpdate') {
for (const member of context.activity.membersAdded) {
if (member.id === context.activity.recipient.id) {
await YourRootTopic.start(context, startArgs, constructorArgs);
}
}
await YourRootTopic.dispatch(context);
});
This is such a common pattern that there's a helper:
yourMessageLoop(context => doTopic(YourRootTopic, context, startArgs, constructorArgs));
In addition to helping your application implement the Topics abstraction, Topical has a few helpers which make life easier for you bot builders:
consoleOnTurn
wrapsConsoleAdapter
'slisten
method, injecting in aconversationUpdate
activity at the start of the conversation. This helps you share the same bot logic between Console bots and Bot Framework bots.
In every topic:
this.text
is a shorthand forthis.context.activity.text.trim()
-- it isundefined
if the activity type is notmessage
this.send
is a shorthand forthis.context.sendActivity
Topic shouldn't reimplement every part of this.context
-- but let's all look for places where it can make life easier.
These helpers are used throughout the samples.
To these samples, you'll need to clone this repo.
simple sample is a JavaScript bot that demonstrates a conversation with parent-child topics. Run node samples/simple.js
The rest of the samples are written in TypeScript. To run them you'll need to:
npm install
npm run build
Note: all these are console bots, and use the helper consoleOnTurn
described below.
alarm bot has a little more depth. Run node lib/samples/alarmBot.js
custom context demonstrates the use of a custom TurnContext
. Run node lib/samples/customContext.js
culture demonstrates the use of a custom prompt validator and NumberPrompt
, which requires providing a constructor argument. Run node lib/samples/culture.js
triggers demonstrates the use of triggers. Run node lib/samples/triggers.js
knock knock demonstrates the use of a simple waterfall. Run node lib/samples/knockKnock.js
waterfall demonstrates the use of Prompts in a waterfall. Run node lib/samples/waterfall.js
dispatch demonstrates saving the user's input to be dispatched subsequently. Run node lib/samples/dispatch.js
choices demonstrates prompting with choices. Run node lib/samples/choices.js
Read about different patterns that can be implemented with Topical.
Learn about prompts and waterfalls.
Learn how Topical works.
Learn the Topical API in the reference.