8000 Merge pull request #204 from microsoft/Zerryth/DialogPrompts · aiedward/botbuilder-python@969442f · GitHub
[go: up one dir, main page]

Skip to content

Commit 969442f

Browse files
authored
Merge pull request microsoft#204 from microsoft/Zerryth/DialogPrompts
Zerryth/dialog prompts
2 parents 6be1edb + 923eb38 commit 969442f

28 files changed

+2367
-133
lines changed

libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ async def receive_activity(self, activity):
134134
if value is not None and key != 'additional_properties':
135135
setattr(request, key, value)
136136

137-
request.type = ActivityTypes.message
137+
request.type = request.type or ActivityTypes.message
138138
if not request.id:
139139
self._next_id += 1
140140
request.id = str(self._next_id)
@@ -143,6 +143,9 @@ async def receive_activity(self, activity):
143143
context = TurnContext(self, request)
144144
return await self.run_pipeline(context, self.logic)
145145

146+
def get_next_activity(self) -> Activity:
147+
return self.activity_buffer.pop(0)
148+
146149
async def send(self, user_says) -> object:
147150
"""
148151
Sends something to the bot. This returns a new `TestFlow` instance which can be used to add
@@ -300,7 +303,7 @@ def default_inspector(reply, description=None):
300303
validate_activity(reply, expected)
301304
else:
302305
assert reply.type == 'message', description + f" type == {reply.type}"
303-
assert reply.text == expected, description + f" text == {reply.text}"
306+
assert reply.text.strip() == expected.strip(), description + f" text == {reply.text}"
304307

305308
if description is None:
306309
description = ''

libraries/botbuilder-core/botbuilder/core/turn_context.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,8 @@ async def send_activity(self, *activity_or_text: Union[Activity, str]) -> Resour
140140
Activity(text=a, type='message') if isinstance(a, str) else a, reference)
141141
for a in activity_or_text]
142142
for activity in output:
143-
activity.input_hint = 'acceptingInput'
143+
if not activity.input_hint:
144+
activity.input_hint = 'acceptingInput'
144145

145146
async def callback(context: 'TurnContext', output):
146147
responses = await context.adapter.send_activities(context, output)

libraries/botbuilder-dialogs/botbuilder/dialogs/choices/__init__.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,30 @@
99
from .choice import Choice
1010
from .choice_factory_options import ChoiceFactoryOptions
1111
from .choice_factory import ChoiceFactory
12+
from .choice_recognizers import ChoiceRecognizers
13+
from .find import Find
14+
from .find_choices_options import FindChoicesOptions, FindValuesOptions
15+
from .found_choice import FoundChoice
16+
from .found_value import FoundValue
1217
from .list_style import ListStyle
18+
from .model_result import ModelResult
19+
from .sorted_value import SortedValue
20+
from .token import Token
21+
from .tokenizer import Tokenizer
1322

14-
__all__ = ["Channel", "Choice", "ChoiceFactory", "ChoiceFactoryOptions", "ListStyle"]
23+
__all__ = [
24+
"Channel",
25+
"Choice",
26+
"ChoiceFactory",
27+
"ChoiceFactoryOptions",
28+
"ChoiceRecognizers",
29+
"Find",
30+
"FindChoicesOptions",
31+
"FindValuesOptions",
32+
"FoundChoice",
33+
"ListStyle",
34+
"ModelResult",
35+
"SortedValue",
36+
"Token",
37+
"Tokenizer"
38+
]

libraries/botbuilder-dialogs/botbuilder/dialogs/choices/channel.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
class Channel(object):
99
"""
10-
Methods for determining channel specific functionality.
10+
Methods for determining channel-specific functionality.
1111
"""
1212

1313
@staticmethod

libraries/botbuilder-dialogs/botbuilder/dialogs/choices/choice_factory.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010

1111

1212
class ChoiceFactory:
13+
"""
14+
Assists with formatting a message activity that contains a list of choices.
15+
"""
1316
@staticmethod
1417
def for_channel(
1518
channel_id: str,
@@ -18,6 +21,20 @@ def for_channel(
1821
speak: str = None,
1922
options: ChoiceFactoryOptions = None,
2023
) -> Activity:
24+
"""
25+
Creates a message activity that includes a list of choices formatted based on the capabilities of a given channel.
26+
27+
Parameters:
28+
----------
29+
30+
channel_id: A channel ID.
31+
32+
choices: List of choices to render.
33+
34+
text: (Optional) Text of the message to send.
35+
36+
speak (Optional) SSML. Text to be spoken by your bot on a speech-enabled channel.
37+
"""
2138
if channel_id is None:
2239
channel_id = ""
2340

@@ -65,6 +82,20 @@ def inline(
6582
speak: str = None,
6683
options: ChoiceFactoryOptions = None,
6784
) -> Activity:
85+
"""
86+
Creates a message activity that includes a list of choices formatted as an inline list.
87+
88+
Parameters:
89+
----------
90+
91+
choices: The list of choices to render.
92+
93+
text: (Optional) The text of the message to send.
94+
95+
speak: (Optional) SSML. Text to be spoken by your bot on a speech-enabled channel.
96+
97+
options: (Optional) The formatting options to use to tweak rendering of list.
98+
"""
6899
if choices is None:
69100
choices = []
70101

@@ -113,6 +144,20 @@ def list_style(
113144
speak: str = None,
114145
options: ChoiceFactoryOptions = None,
115146
):
147+
"""
148+
Creates a message activity that includes a list of choices formatted as a numbered or bulleted list.
149+
150+
Parameters:
151+
----------
152+
153+
choices: The list of choices to render.
154+
155+
text: (Optional) The text of the message to send.
156+
157+
speak: (Optional) SSML. Text to be spoken by your bot on a speech-enabled channel.
158+
159+
options: (Optional) The formatting options to use to tweak rendering of list.
160+
"""
116161
if choices is None:
117162
choices = []
118163
if options is None:
@@ -153,6 +198,9 @@ def list_style(
153198
def suggested_action(
154199
choices: List[Choice], text: str = None, speak: str = None
155200
) -> Activity:
201+
"""
202+
Creates a message activity that includes a list of choices that have been added as suggested actions.
203+
"""
156204
# Return activity with choices as suggested actions
157205
return MessageFactory.suggested_actions(
158206
ChoiceFactory._extract_actions(choices),
@@ -165,6 +213,9 @@ def suggested_action(
165213
def hero_card(
166214
choices: List[Choice], text: str = None, speak: str = None
167215
) -> Activity:
216+
"""
217+
Creates a message activity that includes a lsit of coices that have been added as `HeroCard`'s
218+
"""
168219
attachment = CardFactory.hero_card(
169220
HeroCard(text=text, buttons=ChoiceFactory._extract_actions(choices))
170221
)
@@ -176,6 +227,9 @@ def hero_card(
176227

177228
@staticmethod
178229
def _to_choices(choices: List[str]) -> List[Choice]:
230+
"""
231+
Takes a list of strings and returns them as [`Choice`].
232+
"""
179233
if choices is None:
180234
return []
181235
else:
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from recognizers_number import NumberModel, NumberRecognizer, OrdinalModel
5+
from recognizers_text import Culture
6+
from typing import List, Union
7+
8+
9+
from .choice import Choice
10+
from .find import Find
11+
from .find_choices_options import FindChoicesOptions
12+
from .found_choice import FoundChoice
13+
from .model_result import ModelResult
14+
15+
class ChoiceRecognizers:
16+
""" Contains methods for matching user input against a list of choices. """
17+
18+
@staticmethod
19+
def recognize_choices(
20+
utterance: str,
21+
choices: List[Union[str, Choice]],
22+
options: FindChoicesOptions = None
23+
) -> List[ModelResult]:
24+
"""
25+
Matches user input against a list of choices.
26+
27+
This is layered above the `Find.find_choices()` function, and adds logic to let the user specify
28+
their choice by index (they can say "one" to pick `choice[0]`) or ordinal position (they can say "the second one" to pick `choice[1]`.)
29+
The user's utterance is recognized in the following order:
30+
31+
- By name using `find_choices()`
32+
- By 1's based ordinal position.
33+
- By 1's based index position.
34+
35+
Parameters:
36+
-----------
37+
38+
utterance: The input.
39+
40+
choices: The list of choices.
41+
42+
options: (Optional) Options to control the recognition strategy.
43+
44+
Returns:
45+
--------
46+
A list of found choices, sorted by most relevant first.
47+
"""
48+
if utterance is None:
49+
utterance = ''
50+
51+
# Normalize list of choices
52+
choices_list = [Choice(value=choice) if isinstance(choice, str) else choice for choice in choices]
53+
54+
# Try finding choices by text search first
55+
# - We only want to use a single strategy for returning results to avoid issues where utterances
56+
# like the "the third one" or "the red one" or "the first division book" would miss-recognize as
57+
# a numerical index or ordinal as well.
58+
locale = options.locale if (options and options.locale) else Culture.English
59+
matched = Find.find_choices(utterance, choices_list, options)
60+
if len(matched) == 0:
61+
# Next try finding by ordinal
62+
matches = ChoiceRecognizers._recognize_ordinal(utterance, locale)
63+
64+
if len(matches) > 0:
65+
for match in matches:
66+
ChoiceRecognizers._match_choice_by_index(choices_list, matched, match)
67+
else:
68+
# Finally try by numerical index
69+
matches = ChoiceRecognizers._recognize_number(utterance, locale)
70+
71+
for match in matches:
72+
ChoiceRecognizers._match_choice_by_index(choices_list, matched, match)
73+
74+
# Sort any found matches by their position within the utterance.
75+
# - The results from find_choices() are already properly sorted so we just need this
76+
# for ordinal & numerical lookups.
77+
matched = sorted(
78+
matched,
79+
key=lambda model_result: model_result.start
80+
)
81+
82+
return matched
83+
84+
85+
@staticmethod
86+
def _recognize_ordinal(utterance: str, culture: str) -> List[ModelResult]:
87+
model: OrdinalModel = NumberRecognizer(culture).get_ordinal_model(culture)
88+
89+
return list(map(ChoiceRecognizers._found_choice_constructor, model.parse(utterance)))
90+
91+
@staticmethod
92+
def _match_choice_by_index(
93+
choices: List[Choice],
94+
matched: List[ModelResult],
95+
match: ModelResult
96+
):
97+
try:
98+
index: int = int(match.resolution.value) - 1
99+
if (index >= 0 and index < len(choices)):
100+
choice = choices[index]
101+
102+
matched.append(ModelResult(
103+
start=match.start,
104+
end=match.end,
105+
type_name='choice',
106+
text=match.text,
107+
resolution=FoundChoice(
108+
value=choice.value,
109+
index=index,
110+
score=1.0
111+
)
112+
))
113+
except:
114+
# noop here, as in dotnet/node repos
115+
pass
116+
117+
@staticmethod
118+
def _recognize_number(utterance: str, culture: str) -> List[ModelResult]:
119+
model: NumberModel = NumberRecognizer(culture).get_number_model(culture)
120+
121+
return list(map(ChoiceRecognizers._found_choice_constructor, model.parse(utterance)))
122+
123+
@staticmethod
124+
def _found_choice_constructor(value_model: ModelResult) -> ModelResult:
125+
return ModelResult(
126+
start=value_model.start,
127+
end=value_model.end,
128+
type_name='choice',
129+
text=value_model.text,
130+
resolution=FoundChoice(
131+
value=value_model.resolution['value'],
132+
index=0,
133+
score=1.0,
134+
)
135+
)
136+
137+

0 commit comments

Comments
 (0)
0