Skip to content

Commit e3708f9

Browse files
tracyboehreraxelsrz
authored andcommitted
Added 19.custom-dialogs (#411)
1 parent 0f524c3 commit e3708f9

12 files changed

+479
-0
lines changed

samples/19.custom-dialogs/README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Custom Dialogs
2+
3+
Bot Framework v4 custom dialogs bot sample
4+
5+
This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to sub-class the `Dialog` class to create different bot control mechanism like simple slot filling.
6+
7+
BotFramework provides a built-in base class called `Dialog`. By subclassing `Dialog`, developers can create new ways to define and control dialog flows used by the bot.
8+
9+
## Running the sample
10+
- Clone the repository
11+
```bash
12+
git clone https://github.com/Microsoft/botbuilder-python.git
13+
```
14+
- Activate your desired virtual environment
15+
- Bring up a terminal, navigate to `botbuilder-python\samples\19.custom-dialogs` folder
16+
- In the terminal, type `pip install -r requirements.txt`
17+
- In the terminal, type `python app.py`
18+
19+
## Testing the bot using Bot Framework Emulator
20+
[Microsoft Bot Framework Emulator](https://github.com/microsoft/botframework-emulator) is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel.
21+
22+
- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases)
23+
24+
### Connect to bot using Bot Framework Emulator
25+
- Launch Bot Framework Emulator
26+
- File -> Open Bot
27+
- Paste this URL in the emulator window - http://localhost:3978/api/messages
28+
29+
## Custom Dialogs
30+
31+
BotFramework provides a built-in base class called `Dialog`. By subclassing Dialog, developers
32+
can create new ways to define and control dialog flows used by the bot. By adhering to the
33+
features of this class, developers will create custom dialogs that can be used side-by-side
34+
with other dialog types, as well as built-in or custom prompts.
35+
36+
This example demonstrates a custom Dialog class called `SlotFillingDialog`, which takes a
37+
series of "slots" which define a value the bot needs to collect from the user, as well
38+
as the prompt it should use. The bot will iterate through all of the slots until they are
39+
all full, at which point the dialog completes.
40+
41+
# Further reading
42+
43+
- [Bot Framework Documentation](https://docs.botframework.com)
44+
- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0)
45+
- [Dialogs](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0)
46+
- [Dialog class reference](https://docs.microsoft.com/en-us/javascript/api/botbuilder-dialogs/dialog)
47+
- [Manage complex conversation flows with dialogs](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-dialog-manage-complex-conversation-flow?view=azure-bot-service-4.0)
48+
- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0)

samples/19.custom-dialogs/app.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import asyncio
5+
import sys
6+
from datetime import datetime
7+
from types import MethodType
8+
9+
from flask import Flask, request, Response
10+
from botbuilder.core import (
11+
BotFrameworkAdapter,
12+
BotFrameworkAdapterSettings,
13+
ConversationState,
14+
MemoryStorage,
15+
TurnContext,
16+
UserState,
17+
)
18+
from botbuilder.schema import Activity, ActivityTypes
19+
20+
from bots import DialogBot
21+
22+
# Create the loop and Flask app
23+
from dialogs.root_dialog import RootDialog
24+
25+
LOOP = asyncio.get_event_loop()
26+
APP = Flask(__name__, instance_relative_config=True)
27+
APP.config.from_object("config.DefaultConfig")
28+
29+
# Create adapter.
30+
# See https://aka.ms/about-bot-adapter to learn more about how bots work.
31+
SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"])
32+
ADAPTER = BotFrameworkAdapter(SETTINGS)
33+
34+
35+
# Catch-all for errors.
36+
async def on_error(self, context: TurnContext, error: Exception):
37+
# This check writes out errors to console log .vs. app insights.
38+
# NOTE: In production environment, you should consider logging this to Azure
39+
# application insights.
40+
print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr)
41+
42+
# Send a message to the user
43+
await context.send_activity("The bot encountered an error or bug.")
44+
await context.send_activity("To continue to run this bot, please fix the bot source code.")
45+
# Send a trace activity if we're talking to the Bot Framework Emulator
46+
if context.activity.channel_id == 'emulator':
47+
# Create a trace activity that contains the error object
48+
trace_activity = Activity(
49+
label="TurnError",
50+
name="on_turn_error Trace",
51+
timestamp=datetime.utcnow(),
52+
type=ActivityTypes.trace,
53+
value=f"{error}",
54+
value_type="https://www.botframework.com/schemas/error"
55+
)
56+
# Send a trace activity, which will be displayed in Bot Framework Emulator
57+
await context.send_activity(trace_activity)
58+
59+
ADAPTER.on_turn_error = MethodType(on_error, ADAPTER)
60+
61+
# Create MemoryStorage and state
62+
MEMORY = MemoryStorage()
63+
USER_STATE = UserState(MEMORY)
64+
CONVERSATION_STATE = ConversationState(MEMORY)
65+
66+
# Create Dialog and Bot
67+
DIALOG = RootDialog(USER_STATE)
68+
BOT = DialogBot(CONVERSATION_STATE, USER_STATE, DIALOG)
69+
70+
71+
# Listen for incoming requests on /api/messages.
72+
@APP.route("/api/messages", methods=["POST"])
73+
def messages():
74+
# Main bot message handler.
75+
if "application/json" in request.headers["Content-Type"]:
76+
body = request.json
77+
else:
78+
return Response(status=415)
79+
80+
activity = Activity().deserialize(body)
81+
auth_header = (
82+
request.headers["Authorization"] if "Authorization" in request.headers else ""
83+
)
84+
85+
try:
86+
task = LOOP.create_task(
87+
ADAPTER.process_activity(activity, auth_header, BOT.on_turn)
88+
)
89+
LOOP.run_until_complete(task)
90+
return Response(status=201)
91+
except Exception as exception:
92+
raise exception
93+
94+
95+
if __name__ == "__main__":
96+
try:
97+
APP.run(debug=False, port=APP.config["PORT"]) # nosec debug
98+
except Exception as exception:
99+
raise exception
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from .dialog_bot import DialogBot
5+
6+
__all__ = ["DialogBot"]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from botbuilder.core import ActivityHandler, ConversationState, TurnContext, UserState
5+
from botbuilder.dialogs import Dialog
6+
7+
from helpers.dialog_helper import DialogHelper
8+
9+
10+
class DialogBot(ActivityHandler):
11+
def __init__(self, conversation_state: ConversationState, user_state: UserState, dialog: Dialog):
12+
self.conversation_state = conversation_state
13+
self.user_state = user_state
14+
self.dialog = dialog
15+
16+
async def on_turn(self, turn_context: TurnContext):
17+
await super().on_turn(turn_context)
18+
19+
# Save any state changes that might have occurred during the turn.
20+
await self.conversation_state.save_changes(turn_context)
21+
await self.user_state.save_changes(turn_context)
22+
23+
async def on_message_activity(self, turn_context: TurnContext):
24+
# Run the Dialog with the new message Activity.
25+
await DialogHelper.run_dialog(
26+
self.dialog,
27+
turn_context,
28+
self.conversation_state.create_property("DialogState"),
29+
)

samples/19.custom-dialogs/config.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License.
4+
5+
import os
6+
7+
""" Bot Configuration """
8+
9+
10+
class DefaultConfig:
11+
""" Bot Configuration """
12+
13+
PORT = 3978
14+
APP_ID = os.environ.get("MicrosoftAppId", "")
15+
APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from .slot_filling_dialog import SlotFillingDialog
5+
from .root_dialog import RootDialog
6+
7+
__all__ = ["RootDialog", "SlotFillingDialog"]
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from typing import Dict
5+
6+
from botbuilder.dialogs import (
7+
ComponentDialog,
8+
WaterfallDialog,
9+
WaterfallStepContext,
10+
DialogTurnResult,
11+
NumberPrompt, PromptValidatorContext)
12+
from botbuilder.dialogs.prompts import TextPrompt
13+
from botbuilder.core import MessageFactory, UserState
14+
from recognizers_text import Culture
15+
16+
from dialogs import SlotFillingDialog
17+
from dialogs.slot_details import SlotDetails
18+
19+
20+
class RootDialog(ComponentDialog):
21+
def __init__(
22+
self, user_state: UserState
23+
):
24+
super(RootDialog, self).__init__(RootDialog.__name__)
25+
26+
self.user_state_accessor = user_state.create_property("result")
27+
28+
# Rather than explicitly coding a Waterfall we have only to declare what properties we want collected.
29+
# In this example we will want two text prompts to run, one for the first name and one for the last
30+
fullname_slots = [
31+
SlotDetails(
32+
name="first",
33+
dialog_id="text",
34+
prompt="Please enter your first name."
35+
),
36+
SlotDetails(
37+
name="last",
38+
dialog_id="text",
39+
prompt="Please enter your last name."
40+
)
41+
]
42+
43+
# This defines an address dialog that collects street, city and zip properties.
44+
address_slots = [
45+
SlotDetails(
46+
name="street",
47+
dialog_id="text",
48+
prompt="Please enter the street address."
49+
),
50+
SlotDetails(
51+
name="city",
52+
dialog_id="text",
53+
prompt="Please enter the city."
54+
),
55+
SlotDetails(
56+
name="zip",
57+
dialog_id="text",
58+
prompt="Please enter the zip."
59+
)
60+
]
61+
62+
# Dialogs can be nested and the slot filling dialog makes use of that. In this example some of the child
63+
# dialogs are slot filling dialogs themselves.
64+
slots = [
65+
SlotDetails(
66+
name="fullname",
67+
dialog_id="fullname",
68+
),
69+
SlotDetails(
70+
name="age",
71+
dialog_id="number",
72+
prompt="Please enter your age."
73+
),
74+
SlotDetails(
75+
name="shoesize",
76+
dialog_id="shoesize",
77+
prompt="Please enter your shoe size.",
78+
retry_prompt="You must enter a size between 0 and 16. Half sizes are acceptable."
79+
),
80+
SlotDetails(
81+
name="address",
82+
dialog_id="address"
83+
)
84+
]
85+
86+
# Add the various dialogs that will be used to the DialogSet.
87+
self.add_dialog(SlotFillingDialog("address", address_slots))
88+
self.add_dialog(SlotFillingDialog("fullname", fullname_slots))
89+
self.add_dialog(TextPrompt("text"))
90+
self.add_dialog(NumberPrompt("number", default_locale=Culture.English))
91+
self.add_dialog(NumberPrompt("shoesize", RootDialog.shoe_size_validator, default_locale=Culture.English))
92+
self.add_dialog(SlotFillingDialog("slot-dialog", slots))
93+
94+
# Defines a simple two step Waterfall to test the slot dialog.
95+
self.add_dialog(
96+
WaterfallDialog(
97+
"waterfall", [self.start_dialog, self.process_result]
98+
)
99+
)
100+
101+
# The initial child Dialog to run.
102+
self.initial_dialog_id = "waterfall"
103+
104+
async def start_dialog(self, step_context: WaterfallStepContext) -> DialogTurnResult:
105+
# Start the child dialog. This will run the top slot dialog than will complete when all the properties are
106+
# gathered.
107+
return await step_context.begin_dialog("slot-dialog")
108+
109+
async def process_result(self, step_context: WaterfallStepContext) -> DialogTurnResult:
110+
# To demonstrate that the slot dialog collected all the properties we will echo them back to the user.
111+
if type(step_context.result) is dict and len(step_context.result) > 0:
112+
fullname: Dict[str, object] = step_context.result["fullname"]
113+
shoe_size: float = step_context.result["shoesize"]
114+
address: dict = step_context.result["address"]
115+
116+
# store the response on UserState
117+
obj: dict = await self.user_state_accessor.get(step_context.context, dict)
118+
obj["data"] = {}
119+
obj["data"]["fullname"] = f"{fullname.get('first')} {fullname.get('last')}"
120+
obj["data"]["shoesize"] = f"{shoe_size}"
121+
obj["data"]["address"] = f"{address['street']}, {address['city']}, {address['zip']}"
122+
123+
# show user the values
124+
await step_context.context.send_activity(MessageFactory.text(obj["data"]["fullname"]))
125+
await step_context.context.send_activity(MessageFactory.text(obj["data"]["shoesize"]))
126+
await step_context.context.send_activity(MessageFactory.text(obj["data"]["address"]))
127+
128+
return await step_context.end_dialog()
129+
130+
@staticmethod
131+
async def shoe_size_validator(prompt_context: PromptValidatorContext) -> bool:
132+
shoe_size = round(prompt_context.recognized.value, 1)
133+
134+
# show sizes can range from 0 to 16, whole or half sizes only
135+
if 0 <= shoe_size <= 16 and (shoe_size * 2) % 1 == 0:
136+
prompt_context.recognized.value = shoe_size
137+
return True
138+
return False
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from botbuilder.core import MessageFactory
5+
from botbuilder.dialogs import PromptOptions
6+
7+
8+
class SlotDetails:
9+
def __init__(self,
10+
name: str,
11+
dialog_id: str,
12+
options: PromptOptions = None,
13+
prompt: str = None,
14+
retry_prompt: str = None
15+
):
16+
self.name = name
17+
self.dialog_id = dialog_id
18+
self.options = options if options else PromptOptions(
19+
prompt=MessageFactory.text(prompt),
20+
retry_prompt=None if retry_prompt is None else MessageFactory.text(retry_prompt)
21+
)

0 commit comments

Comments
 (0)