Skip to content

Added 19.custom-dialogs #411

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions samples/19.custom-dialogs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Custom Dialogs

Bot Framework v4 custom dialogs bot sample

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.

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.

## Running the sample
- Clone the repository
```bash
git clone https://github.com/Microsoft/botbuilder-python.git
```
- Activate your desired virtual environment
- Bring up a terminal, navigate to `botbuilder-python\samples\19.custom-dialogs` folder
- In the terminal, type `pip install -r requirements.txt`
- In the terminal, type `python app.py`

## Testing the bot using Bot Framework Emulator
[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.

- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases)

### Connect to bot using Bot Framework Emulator
- Launch Bot Framework Emulator
- File -> Open Bot
- Paste this URL in the emulator window - http://localhost:3978/api/messages

## Custom Dialogs

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. By adhering to the
features of this class, developers will create custom dialogs that can be used side-by-side
with other dialog types, as well as built-in or custom prompts.

This example demonstrates a custom Dialog class called `SlotFillingDialog`, which takes a
series of "slots" which define a value the bot needs to collect from the user, as well
as the prompt it should use. The bot will iterate through all of the slots until they are
all full, at which point the dialog completes.

# Further reading

- [Bot Framework Documentation](https://docs.botframework.com)
- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0)
- [Dialogs](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-dialog?view=azure-bot-service-4.0)
- [Dialog class reference](https://docs.microsoft.com/en-us/javascript/api/botbuilder-dialogs/dialog)
- [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)
- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0)
99 changes: 99 additions & 0 deletions samples/19.custom-dialogs/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import asyncio
import sys
from datetime import datetime
from types import MethodType

from flask import Flask, request, Response
from botbuilder.core import (
BotFrameworkAdapter,
BotFrameworkAdapterSettings,
ConversationState,
MemoryStorage,
TurnContext,
UserState,
)
from botbuilder.schema import Activity, ActivityTypes

from bots import DialogBot

# Create the loop and Flask app
from dialogs.root_dialog import RootDialog

LOOP = asyncio.get_event_loop()
APP = Flask(__name__, instance_relative_config=True)
APP.config.from_object("config.DefaultConfig")

# Create adapter.
# See https://aka.ms/about-bot-adapter to learn more about how bots work.
SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"])
ADAPTER = BotFrameworkAdapter(SETTINGS)


# Catch-all for errors.
async def on_error(self, context: TurnContext, error: Exception):
# This check writes out errors to console log .vs. app insights.
# NOTE: In production environment, you should consider logging this to Azure
# application insights.
print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr)

# Send a message to the user
await context.send_activity("The bot encountered an error or bug.")
await context.send_activity("To continue to run this bot, please fix the bot source code.")
# Send a trace activity if we're talking to the Bot Framework Emulator
if context.activity.channel_id == 'emulator':
# Create a trace activity that contains the error object
trace_activity = Activity(
label="TurnError",
name="on_turn_error Trace",
timestamp=datetime.utcnow(),
type=ActivityTypes.trace,
value=f"{error}",
value_type="https://www.botframework.com/schemas/error"
)
# Send a trace activity, which will be displayed in Bot Framework Emulator
await context.send_activity(trace_activity)

ADAPTER.on_turn_error = MethodType(on_error, ADAPTER)

# Create MemoryStorage and state
MEMORY = MemoryStorage()
USER_STATE = UserState(MEMORY)
CONVERSATION_STATE = ConversationState(MEMORY)

# Create Dialog and Bot
DIALOG = RootDialog(USER_STATE)
BOT = DialogBot(CONVERSATION_STATE, USER_STATE, DIALOG)


# Listen for incoming requests on /api/messages.
@APP.route("/api/messages", methods=["POST"])
def messages():
# Main bot message handler.
if "application/json" in request.headers["Content-Type"]:
body = request.json
else:
return Response(status=415)

activity = Activity().deserialize(body)
auth_header = (
request.headers["Authorization"] if "Authorization" in request.headers else ""
)

try:
task = LOOP.create_task(
ADAPTER.process_activity(activity, auth_header, BOT.on_turn)
)
LOOP.run_until_complete(task)
return Response(status=201)
except Exception as exception:
raise exception


if __name__ == "__main__":
try:
APP.run(debug=False, port=APP.config["PORT"]) # nosec debug
except Exception as exception:
raise exception
6 changes: 6 additions & 0 deletions samples/19.custom-dialogs/bots/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from .dialog_bot import DialogBot

__all__ = ["DialogBot"]
29 changes: 29 additions & 0 deletions samples/19.custom-dialogs/bots/dialog_bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from botbuilder.core import ActivityHandler, ConversationState, TurnContext, UserState
from botbuilder.dialogs import Dialog

from helpers.dialog_helper import DialogHelper


class DialogBot(ActivityHandler):
def __init__(self, conversation_state: ConversationState, user_state: UserState, dialog: Dialog):
self.conversation_state = conversation_state
self.user_state = user_state
self.dialog = dialog

async def on_turn(self, turn_context: TurnContext):
await super().on_turn(turn_context)

# Save any state changes that might have occurred during the turn.
await self.conversation_state.save_changes(turn_context)
await self.user_state.save_changes(turn_context)

async def on_message_activity(self, turn_context: TurnContext):
# Run the Dialog with the new message Activity.
await DialogHelper.run_dialog(
self.dialog,
turn_context,
self.conversation_state.create_property("DialogState"),
)
15 changes: 15 additions & 0 deletions samples/19.custom-dialogs/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env python3
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import os

""" Bot Configuration """


class DefaultConfig:
""" Bot Configuration """

PORT = 3978
APP_ID = os.environ.get("MicrosoftAppId", "")
APP_PASSWORD = os.environ.get("MicrosoftAppPassword", "")
7 changes: 7 additions & 0 deletions samples/19.custom-dialogs/dialogs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from .slot_filling_dialog import SlotFillingDialog
from .root_dialog import RootDialog

__all__ = ["RootDialog", "SlotFillingDialog"]
138 changes: 138 additions & 0 deletions samples/19.custom-dialogs/dialogs/root_dialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from typing import Dict

from botbuilder.dialogs import (
ComponentDialog,
WaterfallDialog,
WaterfallStepContext,
DialogTurnResult,
NumberPrompt, PromptValidatorContext)
from botbuilder.dialogs.prompts import TextPrompt
from botbuilder.core import MessageFactory, UserState
from recognizers_text import Culture

from dialogs import SlotFillingDialog
from dialogs.slot_details import SlotDetails


class RootDialog(ComponentDialog):
def __init__(
self, user_state: UserState
):
super(RootDialog, self).__init__(RootDialog.__name__)

self.user_state_accessor = user_state.create_property("result")

# Rather than explicitly coding a Waterfall we have only to declare what properties we want collected.
# In this example we will want two text prompts to run, one for the first name and one for the last
fullname_slots = [
SlotDetails(
name="first",
dialog_id="text",
prompt="Please enter your first name."
),
SlotDetails(
name="last",
dialog_id="text",
prompt="Please enter your last name."
)
]

# This defines an address dialog that collects street, city and zip properties.
address_slots = [
SlotDetails(
name="street",
dialog_id="text",
prompt="Please enter the street address."
),
SlotDetails(
name="city",
dialog_id="text",
prompt="Please enter the city."
),
SlotDetails(
name="zip",
dialog_id="text",
prompt="Please enter the zip."
)
]

# Dialogs can be nested and the slot filling dialog makes use of that. In this example some of the child
# dialogs are slot filling dialogs themselves.
slots = [
SlotDetails(
name="fullname",
dialog_id="fullname",
),
SlotDetails(
name="age",
dialog_id="number",
prompt="Please enter your age."
),
SlotDetails(
name="shoesize",
dialog_id="shoesize",
prompt="Please enter your shoe size.",
retry_prompt="You must enter a size between 0 and 16. Half sizes are acceptable."
),
SlotDetails(
name="address",
dialog_id="address"
)
]

# Add the various dialogs that will be used to the DialogSet.
self.add_dialog(SlotFillingDialog("address", address_slots))
self.add_dialog(SlotFillingDialog("fullname", fullname_slots))
self.add_dialog(TextPrompt("text"))
self.add_dialog(NumberPrompt("number", default_locale=Culture.English))
self.add_dialog(NumberPrompt("shoesize", RootDialog.shoe_size_validator, default_locale=Culture.English))
self.add_dialog(SlotFillingDialog("slot-dialog", slots))

# Defines a simple two step Waterfall to test the slot dialog.
self.add_dialog(
WaterfallDialog(
"waterfall", [self.start_dialog, self.process_result]
)
)

# The initial child Dialog to run.
self.initial_dialog_id = "waterfall"

async def start_dialog(self, step_context: WaterfallStepContext) -> DialogTurnResult:
# Start the child dialog. This will run the top slot dialog than will complete when all the properties are
# gathered.
return await step_context.begin_dialog("slot-dialog")

async def process_result(self, step_context: WaterfallStepContext) -> DialogTurnResult:
# To demonstrate that the slot dialog collected all the properties we will echo them back to the user.
if type(step_context.result) is dict and len(step_context.result) > 0:
fullname: Dict[str, object] = step_context.result["fullname"]
shoe_size: float = step_context.result["shoesize"]
address: dict = step_context.result["address"]

# store the response on UserState
obj: dict = await self.user_state_accessor.get(step_context.context, dict)
obj["data"] = {}
obj["data"]["fullname"] = f"{fullname.get('first')} {fullname.get('last')}"
obj["data"]["shoesize"] = f"{shoe_size}"
obj["data"]["address"] = f"{address['street']}, {address['city']}, {address['zip']}"

# show user the values
await step_context.context.send_activity(MessageFactory.text(obj["data"]["fullname"]))
await step_context.context.send_activity(MessageFactory.text(obj["data"]["shoesize"]))
await step_context.context.send_activity(MessageFactory.text(obj["data"]["address"]))

return await step_context.end_dialog()

@staticmethod
async def shoe_size_validator(prompt_context: PromptValidatorContext) -> bool:
shoe_size = round(prompt_context.recognized.value, 1)

# show sizes can range from 0 to 16, whole or half sizes only
if 0 <= shoe_size <= 16 and (shoe_size * 2) % 1 == 0:
prompt_context.recognized.value = shoe_size
return True
return False
21 changes: 21 additions & 0 deletions samples/19.custom-dialogs/dialogs/slot_details.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from botbuilder.core import MessageFactory
from botbuilder.dialogs import PromptOptions


class SlotDetails:
def __init__(self,
name: str,
dialog_id: str,
options: PromptOptions = None,
prompt: str = None,
retry_prompt: str = None
):
self.name = name
self.dialog_id = dialog_id
self.options = options if options else PromptOptions(
prompt=MessageFactory.text(prompt),
retry_prompt=None if retry_prompt is None else MessageFactory.text(retry_prompt)
)
Loading