Skip to content

Commit

Permalink
Simple dialogs for hotel's room booking fully implemented
Browse files Browse the repository at this point in the history
  • Loading branch information
Joffreybvn committed Feb 7, 2021
1 parent 692e843 commit 54b8812
Show file tree
Hide file tree
Showing 14 changed files with 350 additions and 130 deletions.
15 changes: 11 additions & 4 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
from botbuilder.core.integration import aiohttp_error_middleware
from botbuilder.schema import Activity, ActivityTypes

from src.dialogs import RoomReservationDialog
from src.dialogs import MainDialog, BookingRoomDialog
from src.nlu import NLU
from src import Bot
from config import Config

Expand Down Expand Up @@ -63,9 +64,15 @@ async def on_error(context: TurnContext, error_: Exception):
CONVERSATION_STATE = ConversationState(MEMORY)
USER_STATE = UserState(MEMORY)

# Create main dialog and bot
DIALOG = RoomReservationDialog(USER_STATE)
bot = Bot(CONVERSATION_STATE, USER_STATE, DIALOG)
# Load the NLU recognizer
nlu = NLU()

# Create the dialogs
dialog_room_reservation = BookingRoomDialog(nlu, USER_STATE)
dialog_main = MainDialog(nlu, USER_STATE, dialog_room_reservation)

# Create the bot
bot = Bot(CONVERSATION_STATE, USER_STATE, dialog_main)


# Direct message API
Expand Down
12 changes: 9 additions & 3 deletions src/bot.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@

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

from .dialogs.utils import Emoji
from .dialogs.helpers import DialogHelper
from .nlu import NLU

nlu = NLU()


class Bot(ActivityHandler):
Expand All @@ -16,6 +15,13 @@ def __init__(self, conversation_state: ConversationState, user_state: UserState,
self.user_state = user_state
self.dialog = dialog

async def on_members_added_activity(self, members_added: [ChannelAccount], turn_context: TurnContext):

# Send an "Hello" to any new user connected to the bot
for member in members_added:
if member.id != turn_context.activity.recipient.id:
await turn_context.send_activity(f"Hello {Emoji.WAVING_HAND.value}")

async def on_turn(self, turn_context: TurnContext):

await super().on_turn(turn_context)
Expand Down
5 changes: 3 additions & 2 deletions src/dialogs/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@

from .room_reservation_dialog import RoomReservationDialog
from .booking_room_dialog import BookingRoomDialog
from .main_dialog import MainDialog

__all__ = ["RoomReservationDialog"]
__all__ = ["BookingRoomDialog", "MainDialog"]
179 changes: 179 additions & 0 deletions src/dialogs/booking_room_dialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@

from botbuilder.schema import ChannelAccount, CardAction, ActionTypes, SuggestedActions, Activity, ActivityTypes
from botbuilder.dialogs import ComponentDialog, WaterfallDialog, WaterfallStepContext, DialogTurnResult
from botbuilder.dialogs.prompts import TextPrompt, NumberPrompt, ChoicePrompt, ConfirmPrompt, AttachmentPrompt, PromptOptions, PromptValidatorContext
from botbuilder.dialogs.choices import Choice
from botbuilder.core import MessageFactory, UserState

from src.nlu import Intent, NLU
from .utils import Emoji
from .helpers import NLUHelper
from .data_models import RoomReservation


class BookingRoomDialog(ComponentDialog):

def __init__(self, nlu_recognizer: NLU, user_state: UserState):
super(BookingRoomDialog, self).__init__(BookingRoomDialog.__name__)

# Load the NLU module
self._nlu_recognizer = nlu_recognizer

# Load the RoomReservation class
self.room_reservation_accessor = user_state.create_property("RoomReservation")

# Setup the waterfall dialog
self.add_dialog(WaterfallDialog("WFBookingDialog", [
self.people_step,
self.duration_step,
self.breakfast_step,
self.summary_step,
]))

# Append the prompts and custom prompts
self.add_dialog(NumberPrompt("PeoplePrompt", BookingRoomDialog.people_prompt_validator))
self.add_dialog(NumberPrompt("DurationPrompt", BookingRoomDialog.duration_prompt_validator))
self.add_dialog(ConfirmPrompt("IsTakingBreakfastPrompt"))

self.initial_dialog_id = "WFBookingDialog"

@staticmethod
async def people_step(step_context: WaterfallStepContext) -> DialogTurnResult:
"""Ask the user: how many people to make the reservation?"""

# Retrieve the booking keywords
booking_keywords: dict = step_context.options
step_context.values['booking_keywords'] = booking_keywords

# If the keyword 'people' exists and is filled, pass the question
if 'people' in booking_keywords and booking_keywords['people'] is not None:
return await step_context.next(booking_keywords['people'])

# Give user suggestions (1 or 2 people).
# The user can still write a custom number of people [1, 4].
options = PromptOptions(
prompt=Activity(

type=ActivityTypes.message,
text="Would you like a single or a double room?",

suggested_actions=SuggestedActions(
actions=[
CardAction(
title="Single",
type=ActionTypes.im_back,
value="Single room (1 people)"
),
CardAction(
title="Double",
type=ActionTypes.im_back,
value="Double room (2 peoples)"
)
]
)
),
retry_prompt=MessageFactory.text(
"Reservations can be made for one to four people only."
)
)

# NumberPrompt - How many people ?
return await step_context.prompt(
"PeoplePrompt",
options
)

@staticmethod
async def duration_step(step_context: WaterfallStepContext) -> DialogTurnResult:
"""Ask the user: how many night to reserve?"""

# Save the number of people
step_context.values["people"] = step_context.result

# Retrieve the keywords
booking_keywords: dict = step_context.values["booking_keywords"]

# If the keyword 'duration' exists and is filled, pass the question
if 'duration' in booking_keywords and booking_keywords['duration'] is not None:
return await step_context.next(booking_keywords['duration'])

# NumberPrompt - How many nights ? (duration)
return await step_context.prompt(
"DurationPrompt",
PromptOptions(
prompt=MessageFactory.text("How long do you want to stay?"),
retry_prompt=MessageFactory.text(
"It is only possible to book from 1 to 7 nights"
),
),
)

@staticmethod
async def breakfast_step(step_context: WaterfallStepContext) -> DialogTurnResult:

# Save the number of nights
step_context.values["duration"] = step_context.result

# Confirm people and duration
await step_context.context.send_activity(
MessageFactory.text(
f"Okay, so {step_context.values['people']} people for {step_context.values['duration']} nights"
)
)

# ConfirmPrompt - Is taking breakfast ?
return await step_context.prompt(
"IsTakingBreakfastPrompt",
PromptOptions(
prompt=MessageFactory.text("Will you be having breakfast?")
),
)

async def summary_step(self, step_context: WaterfallStepContext) -> DialogTurnResult:

# Save if the user take the breakfast (bool)
step_context.values["breakfast"] = step_context.result

# If the user said "Yes":
if step_context.result:

# Confirm breakfast hour
await step_context.context.send_activity(
MessageFactory.text(f"Perfect, breakfast is from 6am to 10am")
)

# Save information to Reservation object
room_reservation = await self.room_reservation_accessor.get(
step_context.context, RoomReservation
)

room_reservation.people = step_context.values["people"]
room_reservation.duration = step_context.values["duration"]
room_reservation.breakfast = step_context.values["breakfast"]

# End the dialog
await step_context.context.send_activity(
MessageFactory.text("Your booking has been made !")
)

return await step_context.end_dialog()

@staticmethod
async def people_prompt_validator(prompt_context: PromptValidatorContext) -> bool:
"""Validate the number of people entered by the user."""

# Restrict people between [1 and 4].
return (
prompt_context.recognized.succeeded
and 1 <= prompt_context.recognized.value <= 4
)

@staticmethod
async def duration_prompt_validator(prompt_context: PromptValidatorContext) -> bool:
"""Validate the number of nights entered by the user."""

# Restrict nights between [1 and 7].
return (
prompt_context.recognized.succeeded
and 1 <= prompt_context.recognized.value <= 7
)
3 changes: 2 additions & 1 deletion src/dialogs/helpers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@

from .dialogs_helper import DialogHelper
from .nlu_helper import NLUHelper

__all__ = ["DialogHelper"]
__all__ = ["DialogHelper", "NLUHelper"]
10 changes: 10 additions & 0 deletions src/dialogs/helpers/nlu_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

from src.nlu import Intent, NLU


class NLUHelper:

@staticmethod
async def execute_nlu_query(nlu_recognizer: NLU, message: str) -> (Intent, dict):

return nlu_recognizer.get_intent(message)
98 changes: 98 additions & 0 deletions src/dialogs/main_dialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@

from botbuilder.schema import InputHints
from botbuilder.dialogs import ComponentDialog, WaterfallDialog, WaterfallStepContext, DialogTurnResult
from botbuilder.dialogs.prompts import TextPrompt, PromptOptions
from botbuilder.core import MessageFactory, UserState

from src.nlu import Intent, NLU
from . import BookingRoomDialog
from .utils import Emoji
from .helpers import NLUHelper


class MainDialog(ComponentDialog):

def __init__(self, nlu_recognizer: NLU, user_state: UserState,
booking_room_dialog: BookingRoomDialog):

super(MainDialog, self).__init__(MainDialog.__name__)

# Load the NLU module
self._nlu_recognizer = nlu_recognizer

# Load the sub-dialogs
self._booking_dialog_id = booking_room_dialog.id

# Setup the waterfall dialog
self.add_dialog(WaterfallDialog(WaterfallDialog.__name__, [
self.intro_step,
self.act_step,
self.final_step
]))

# Append the prompts and custom dialogs, used in the waterfall
self.add_dialog(TextPrompt("ActPrompt"))
self.add_dialog(booking_room_dialog)

self.initial_dialog_id = WaterfallDialog.__name__

@staticmethod
async def intro_step(step_context: WaterfallStepContext) -> DialogTurnResult:
"""
Intro step. Triggered upon any interaction from the user to this bot.
"""

# Ask what to do
message = (
str(step_context.options)
if step_context.options
else "What can I help you with today?"
)

# TextPromp - How can I help you ?
return await step_context.prompt(
"ActPrompt",
PromptOptions(
prompt=MessageFactory.text(message)
),
)

async def act_step(self, step_context: WaterfallStepContext) -> DialogTurnResult:
"""
Act step. Take user response and infer its intention.
Dispatch to the desired sub-dialog
"""

intent, keywords = await NLUHelper.execute_nlu_query(
self._nlu_recognizer, step_context.result
)

# Run the BookingRoomDialog, passing it keywords from nlu
if intent == Intent.BOOK_ROOM:
return await step_context.begin_dialog(self._booking_dialog_id, keywords)

# If no intent was understood, return a didn't understand message
else:
didnt_understand_text = (
"Sorry, I didn't get that. Please try asking in a different way"
)

await step_context.context.send_activity(
MessageFactory.text(
didnt_understand_text, didnt_understand_text, InputHints.ignoring_input
)
)

return await step_context.next(None)

async def final_step(self, step_context: WaterfallStepContext) -> DialogTurnResult:
"""
Final step. Triggered upon sub-dialog completion. Replace the current
dialog by the main dialog to start a new loop of conversation.
"""

# Replace the current dialog back to main dialog
return await step_context.replace_dialog(
self.id,
"What else can I do for you?"
)
Loading

0 comments on commit 54b8812

Please sign in to comment.