8000 Added 44.prompt-users-for-input (#380) · TheCompuGuru/botbuilder-python@4df4e1f · GitHub
[go: up one dir, main page]

Skip to content

Commit 4df4e1f

Browse files
tracyboehreraxelsrz
authored andcommitted
Added 44.prompt-users-for-input (microsoft#380)
1 parent 6731036 commit 4df4e1f

File tree

9 files changed

+335
-0
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Prompt users for input
2+
3+
This sample demonstrates how to create your own prompts with the Python Bot Framework.
4+
The bot maintains conversation state to track and direct the conversation and ask the user questions.
5+
The bot maintains user state to track the user's answers.
6+
7+
## Running the sample
8+
- Clone the repository
9+
```bash
10+
git clone https://github.com/Microsoft/botbuilder-python.git
11+
```
12+
- Bring up a terminal, navigate to `botbuilder-python\samples\44.prompt-user-for-input` folder
13+
- In the terminal, type `pip install -r requirements.txt`
8000 14+
- In the terminal, type `python app.py`
15+
16+
## Testing the bot using Bot Framework Emulator
17+
[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.
18+
19+
- Install the Bot Framework emulator from [here](https://github.com/Microsoft/BotFramework-Emulator/releases)
20+
21+
### Connect to bot using Bot Framework Emulator
22+
- Launch Bot Framework Emulator
23+
- File -> Open Bot
24+
- Paste this URL in the emulator window - http://localhost:3978/api/messages
25+
26+
27+
## Bot State
28+
29+
A key to good bot design is to track the context of a conversation, so that your bot remembers things like the answers to previous questions. Depending on what your bot is used for, you may even need to keep track of state or store information for longer than the lifetime of the conversation. A bot's state is information it remembers in order to respond appropriately to incoming messages. The Bot Builder SDK provides classes for storing and retrieving state data as an object associated with a user or a conversation.
30+
31+
# Further reading
32+
33+
- [Azure Bot Service Introduction](https://docs.microsoft.com/en-us/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0)
34+
- [Bot State](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-storage-concept?view=azure-bot-service-4.0)
35+
- [Write directly to storage](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-v4-storage?view=azure-bot-service-4.0&tabs=csharpechorproperty%2Ccsetagoverwrite%2Ccsetag)
36+
- [Managing conversation and user state](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-v4-state?view=azure-bot-service-4.0)
37+
- [Microsoft Recognizers-Text](https://github.com/Microsoft/Recognizers-Text/tree/master/Python)
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 CustomPromptBot
21+
22+
# Create the loop and Flask app
23+
LOOP = asyncio.get_event_loop()
24+
APP = Flask(__name__, instance_relative_config=True)
25+
APP.config.from_object("config.DefaultConfig")
26+
27+
# Create adapter.
28+
# See https://aka.ms/about-bot-adapter to learn more about how bots work.
29+
SETTINGS = BotFrameworkAdapterSettings(APP.config["APP_ID"], APP.config["APP_PASSWORD"])
30+
ADAPTER = BotFrameworkAdapter(SETTINGS)
31+
32+
33+
# Catch-all for errors.
34+
async def on_error(self, context: TurnContext, error: Exception):
35+
# This check writes out errors to console log .vs. app insights.
36+
# NOTE: In production environment, you should consider logging this to Azure
37+
# application insights.
38+
print(f"\n [on_turn_error] unhandled error: {error}", file=sys.stderr)
39+
40+
# Send a message to the user
41+
await context.send_activity("The bot encountered an error or bug.")
42+
await context.send_activity("To continue to run this bot, please fix the bot source code.")
43+
# Send a trace activity if we're talking to the Bot Framework Emulator
44+
if context.activity.channel_id == 'emulator':
45+
# Create a trace activity that contains the error object
46+
trace_activity = Activity(
47+
label="TurnError",
48+
name="on_turn_error Trace",
49+
timestamp=datetime.utcnow(),
50+
type=ActivityTypes.trace,
51+
value=f"{error}",
52+
value_type="https://www.botframework.com/schemas/error"
53+
)
54+
# Send a trace activity, which will be displayed in Bot Framework Emulator
55+
await context.send_activity(trace_activity)
56+
57+
# Clear out state
58+
await CONVERSATION_STATE.delete(context)
59+
60+
ADAPTER.on_turn_error = MethodType(on_error, ADAPTER)
61+
62+
# Create MemoryStorage and state
63+
MEMORY = MemoryStorage()
64+
USER_STATE = UserState(MEMORY)
65+
CONVERSATION_STATE = ConversationState(MEMORY)
66+
67+
# Create Bot
68+
BOT = CustomPromptBot(CONVERSATION_STATE, USER_STATE)
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 .custom_prompt_bot import CustomPromptBot
5+
6+
__all__ = ["CustomPromptBot"]
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from datetime import datetime
5+
6+
from botbuilder.core import ActivityHandler, ConversationState, TurnContext, UserState, MessageFactory
7+
from recognizers_number import recognize_number, Culture
8+
from recognizers_date_time import recognize_datetime
9+
10+
from data_models import ConversationFlow, Question, UserProfile
11+
12+
13+
class ValidationResult:
14+
def __init__(self, is_valid: bool = False, value: object = None, message: str = None):
15+
self.is_valid = is_valid
16+
self.value = value
17+
self.message = message
18+
19+
20+
class CustomPromptBot(ActivityHandler):
21+
def __init__(self, conversation_state: ConversationState, user_state: UserState):
22+
if conversation_state is None:
23+
raise TypeError(
24+
"[CustomPromptBot]: Missing parameter. conversation_state is required but None was given"
25+
)
26+
if user_state is None:
27+
raise TypeError(
28+
"[CustomPromptBot]: Missing parameter. user_state is required but None was given"
29+
)
30+
31+
self.conversation_state = conversation_state
32+
self.user_state = user_state
33+
34+
self.flow_accessor = self.conversation_state.create_property("ConversationFlow")
35+
self.profile_accessor = self.conversation_state.create_property("UserProfile")
36+
37+
async def on_message_activity(self, turn_context: TurnContext):
38+
# Get the state properties from the turn context.
39+
profile = await self.profile_accessor.get(turn_context, UserProfile)
40+
flow = await self.flow_accessor.get(turn_context, ConversationFlow)
41+
42+
await self._fill_out_user_profile(flow, profile, turn_context)
43+
44+
# Save changes to UserState and ConversationState
45+
await self.conversation_state.save_changes(turn_context)
46+
await self.user_state.save_changes(turn_context)
47+
48+
async def _fill_out_user_profile(self, flow: ConversationFlow, profile: UserProfile, turn_context: TurnContext):
49+
user_input = turn_context.activity.text.strip()
50+
51+
# ask for name
52+
if flow.last_question_asked == Question.NONE:
53+
await turn_context.send_activity(MessageFactory.text("Let's get started. What is your name?"))
54+
flow.last_question_asked = Question.NAME
55+
56+
# validate name then ask for age
57+
elif flow.last_question_asked == Question.NAME:
58+
validate_result = self._validate_name(user_input)
59+
if not validate_result.is_valid:
60+
await turn_context.send_activity(MessageFactory.text(validate_result.message))
61+
else:
62+
profile.name = validate_result.value
63+
await turn_context.send_activity(MessageFactory.text(f"Hi {profile.name}"))
64+
await turn_context.send_activity(MessageFactory.text("How old are you?"))
65+
flow.last_question_asked = Question.AGE
66+
67+
# validate age then ask for date
68+
elif flow.last_question_asked == Question.AGE:
69+
validate_result = self._validate_age(user_input)
70+
if not validate_result.is_valid:
71+
await turn_context.send_activity(MessageFactory.text(validate_result.message))
72+
else:
73+
profile.age = validate_result.value
74+
await turn_context.send_activity(MessageFactory.text(f"I have your age as {profile.age}."))
75+
await turn_context.send_activity(MessageFactory.text("When is your flight?"))
76+
flow.last_question_asked = Question.DATE
77+
78+
# validate date and wrap it up
79+
elif flow.last_question_asked == Question.DATE:
80+
validate_result = self._validate_date(user_input)
81+
if not validate_result.is_valid:
82+
await turn_context.send_activity(MessageFactory.text(validate_result.message))
83+
else:
84+
profile.date = validate_result.value
85+
await turn_context.send_activity(MessageFactory.text(
86+
f"Your cab ride to the airport is scheduled for {profile.date}.")
87+
)
88+
await turn_context.send_activity(MessageFactory.text(
89+
f"Thanks for completing the booking {profile.name}.")
90+
)
91+
await turn_context.send_activity(MessageFactory.text("Type anything to run the bot again."))
92+
flow.last_question_asked = Question.NONE
93+
94+
def _validate_name(self, user_input: str) -> ValidationResult:
95+
if not user_input:
96+
return ValidationResult(is_valid=False, message="Please enter a name that contains at least one character.")
97+
else:
98+
return ValidationResult(is_valid=True, value=user_input)
99+
100+
def _validate_age(self, user_input: str) -> ValidationResult:
101+
# Attempt to convert the Recognizer result to an integer. This works for "a dozen", "twelve", "12", and so on.
102+
# The recognizer returns a list of potential recognition results, if any.
103+
results = recognize_number(user_input, Culture.English)
104+
for result in results:
105+
if "value" in result.resolution:
106+
age = int(result.resolution["value"])
107+
if 18 <= age <= 120:
108+
return ValidationResult(is_valid=True, value=age)
109+
110+
return ValidationResult(is_valid=False, message="Please enter an age between 18 and 120.")
111+
112+
def _validate_date(self, user_input: str) -> ValidationResult:
113+
try:
114+
# Try to recognize the input as a date-time. This works for responses such as "11/14/2018", "9pm",
115+
# "tomorrow", "Sunday at 5pm", and so on. The recognizer returns a list of potential recognition results,
116+
# if any.
117+
results = recognize_datetime(user_input, Culture.English)
118+
for result in results:
119+
for resolution in result.resolution["values"]:
120+
if "value" in resolution:
121+
now = datetime.now()
122+
123+
value = resolution["value"]
124+
if resolution["type"] == "date":
125+
candidate = datetime.strptime(value, "%Y-%m-%d")
126+
elif resolution["type"] == "time":
127+
candidate = datetime.strptime(value, "%H:%M:%S")
128+
candidate = candidate.replace(year=now.year, month=now.month, day=now.day)
129+
else:
130+
candidate = datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
131+
132+
# user response must be more than an hour out
133+
diff = candidate - now
134+
if diff.total_seconds() >= 3600:
135+
return ValidationResult(is_valid=True, value=candidate.strftime("%m/%d/%y @ %H:%M"))
136+
137+
return ValidationResult(is_valid=False, message="I'm sorry, please enter a date at least an hour out.")
138+
except ValueError:
139+
return ValidationResult(is_valid=False, message="I'm sorry, I could not interpret that as an appropriate "
140+
"date. Please enter a date at least an hour out.")
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 .conversation_flow import ConversationFlow, Question
5+
from .user_profile import UserProfile
6+
7+
__all__ = ["ConversationFlow", "Question", "UserProfile"]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from enum import Enum
5+
6+
7+
class Question(Enum):
8+
NAME = 1
9+
AGE = 2
10+
DATE = 3
11+
NONE = 4
12+
13+
14+
class ConversationFlow:
15+
def __init__(
16+
self,
17+
last_question_asked: Question = Question.NONE,
18+
):
19+
self.last_question_asked = last_question_asked
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
5+
class UserProfile:
6+
def __init__(self, name: str = None, age: int = 0, date: str = None):
7+
self.name = name
8+
self.age = age
9+
self.date = date
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
botbuilder-core>=4.4.0b1
2+
flask>=1.0.3
3+
recognizers-text>=1.0.2a1

0 commit comments

Comments
 (0)
0