Skip to content

Commit 76ba270

Browse files
authored
Merge pull request #228 from microsoft/axsuarez/DialogPrompts
Tests for activity prompt.
2 parents dc2db65 + 6c8ae77 commit 76ba270

File tree

5 files changed

+206
-37
lines changed

5 files changed

+206
-37
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ async def receive_activity(self, activity):
103103
if value is not None and key != 'additional_properties':
104104
setattr(request, key, value)
105105

106-
request.type = ActivityTypes.message
106+
request.type = request.type or ActivityTypes.message
107107
if not request.id:
108108
self._next_id += 1
109109
request.id = str(self._next_id)

libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/activity_prompt.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from botbuilder.dialogs import Dialog, DialogContext, DialogInstance, DialogReason, DialogTurnResult
99
from botbuilder.schema import Activity, ActivityTypes, InputHints
1010

11+
from .prompt import Prompt
1112
from .prompt_options import PromptOptions
1213
from .prompt_recognizer_result import PromptRecognizerResult
1314
from .prompt_validator_context import PromptValidatorContext
@@ -36,6 +37,8 @@ def __init__(self, dialog_id: str, validator: Callable[[PromptValidatorContext],
3637
"""
3738
Dialog.__init__(self, dialog_id)
3839

40+
if validator is None:
41+
raise TypeError('validator was expected but received None')
3942
self._validator = validator
4043

4144
async def begin_dialog(self, dc: DialogContext, options: PromptOptions) -> DialogTurnResult:
@@ -45,16 +48,18 @@ async def begin_dialog(self, dc: DialogContext, options: PromptOptions) -> Dialo
4548
raise TypeError('ActivityPrompt.begin_dialog(): Prompt options are required for ActivityPrompts.')
4649

4750
# Ensure prompts have input hint set
48-
if options.prompt != None and not options.prompt.input_hint:
51+
if options.prompt is not None and not options.prompt.input_hint:
4952
options.prompt.input_hint = InputHints.expecting_input
5053

51-
if options.retry_prompt != None and not options.retry_prompt.input_hint:
54+
if options.retry_prompt is not None and not options.retry_prompt.input_hint:
5255
options.retry_prompt.input_hint = InputHints.expecting_input
5356

5457
# Initialize prompt state
5558
state: Dict[str, object] = dc.active_dialog.state
5659
state[self.persisted_options] = options
57-
state[self.persisted_state] = Dict[str, object]
60+
state[self.persisted_state] = {
61+
Prompt.ATTEMPT_COUNT_KEY: 0
62+
}
5863

5964
# Send initial prompt
6065
await self.on_prompt(
@@ -76,9 +81,12 @@ async def continue_dialog(self, dc: DialogContext) -> DialogTurnResult:
7681
options: Dict[str, object] = instance.state[self.persisted_options]
7782
recognized: PromptRecognizerResult = await self.on_recognize(dc.context, state, options)
7883

84+
# Increment attempt count
85+
state[Prompt.ATTEMPT_COUNT_KEY] += 1
86+
7987
# Validate the return value
8088
is_valid = False
81-
if self._validator != None:
89+
if self._validator is not None:
8290
prompt_context = PromptValidatorContext(
8391
dc.context,
8492
recognized,
@@ -125,7 +133,7 @@ async def on_prompt(
125133
context: TurnContext,
126134
state: Dict[str, dict],
127135
options: PromptOptions,
128-
isRetry: bool = False
136+
is_retry: bool = False
129137
):
130138
"""
131139
Called anytime the derived class should send the user a prompt.
@@ -140,11 +148,11 @@ async def on_prompt(
140148
141149
isRetry: If `true` the users response wasn't recognized and the re-prompt should be sent.
142150
"""
143-
if isRetry and options.retry_prompt:
151+
if is_retry and options.retry_prompt:
144152
options.retry_prompt.input_hint = InputHints.expecting_input
145153
await context.send_activity(options.retry_prompt)
146154
elif options.prompt:
147-
options.prompt = InputHints.expecting_input
155+
options.prompt.input_hint = InputHints.expecting_input
148156
await context.send_activity(options.prompt)
149157

150158
async def on_recognize(

libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/prompt.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
""" Base class for all prompts.
2121
"""
2222
class Prompt(Dialog):
23+
ATTEMPT_COUNT_KEY = "AttemptCount"
2324
persisted_options = "options"
2425
persisted_state = "state"
2526
def __init__(self, dialog_id: str, validator: object = None):
@@ -52,7 +53,7 @@ async def begin_dialog(self, dc: DialogContext, options: object) -> DialogTurnRe
5253
# Initialize prompt state
5354
state = dc.active_dialog.state
5455
state[self.persisted_options] = options
55-
state[self.persisted_state] = Dict[str, object]
56+
state[self.persisted_state] = {}
5657

5758
# Send initial prompt
5859
await self.on_prompt(dc.context, state[self.persisted_state], state[self.persisted_options], False)

libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/prompt_validator_context.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
from .prompt_recognizer_result import PromptRecognizerResult
77

88

9-
""" Contextual information passed to a custom `PromptValidator`.
10-
"""
119
class PromptValidatorContext():
1210
def __init__(self, turn_context: TurnContext, recognized: PromptRecognizerResult, state: Dict[str, object], options: PromptOptions):
1311
"""Creates contextual information passed to a custom `PromptValidator`.
@@ -71,3 +69,11 @@ def options(self) -> PromptOptions:
7169
The validator can extend this interface to support additional prompt options.
7270
"""
7371
return self._options
72+
73+
@property
74+
def attempt_count(self) -> int:
75+
"""
76+
Gets the number of times the prompt has been executed.
77+
"""
78+
from botbuilder.dialogs.prompts import Prompt
79+
return self._state.get(Prompt.ATTEMPT_COUNT_KEY, 0)

libraries/botbuilder-dialogs/tests/test_activity_prompt.py

Lines changed: 180 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,189 @@
22
# Licensed under the MIT License.
33

44
import aiounittest
5-
from botbuilder.dialogs.prompts import ActivityPrompt, NumberPrompt, PromptOptions, PromptRecognizerResult
6-
from botbuilder.schema import Activity, InputHints
5+
import unittest
76

8-
from botbuilder.core.turn_context import TurnContext
7+
from typing import Callable
8+
from botbuilder.dialogs.prompts import (ActivityPrompt, NumberPrompt, PromptOptions, PromptRecognizerResult,
9+
PromptValidatorContext)
10+
from botbuilder.schema import Activity, InputHints, ActivityTypes
11+
12+
from botbuilder.core import ConversationState, MemoryStorage, TurnContext, MessageFactory
913
from botbuilder.core.adapters import TestAdapter
14+
from botbuilder.dialogs import DialogSet, DialogTurnStatus, DialogReason
15+
16+
17+
async def validator(prompt_context: PromptValidatorContext):
18+
tester = unittest.TestCase()
19+
tester.assertTrue(prompt_context.attempt_count > 0)
20+
21+
activity = prompt_context.recognized.value
22+
23+
if activity.type == ActivityTypes.event:
24+
if int(activity.value) == 2:
25+
prompt_context.recognized.value = MessageFactory.text(str(activity.value))
26+
return True
27+
else:
28+
await prompt_context.context.send_activity("Please send an 'event'-type Activity with a value of 2.")
29+
30+
return False
31+
1032

1133
class SimpleActivityPrompt(ActivityPrompt):
12-
pass
34+
def __init__(self, dialog_id: str, validator: Callable[[PromptValidatorContext], bool]):
35+
super().__init__(dialog_id, validator)
36+
1337

1438
class ActivityPromptTests(aiounittest.AsyncTestCase):
15-
async def test_does_the_things(self):
16-
my_activity = Activity(type='message', text='I am activity message!')
17-
my_retry_prompt = Activity(type='message', id='ididretry', text='retry text hurrr')
18-
options = PromptOptions(prompt=my_activity, retry_prompt=my_retry_prompt)
19-
activity_promptyy = ActivityPrompt('myId', 'validator thing')
20-
21-
my_context = TurnContext(TestAdapter(), my_activity)
22-
my_state = {'stringy': {'nestedkey': 'nestedvalue'} }
23-
24-
await activity_promptyy.on_prompt(my_context, state=my_state, options=options, isRetry=True)
25-
26-
print('placeholder print')
27-
28-
pass
29-
30-
# def test_activity_prompt_with_empty_id_should_fail(self):
31-
# empty_id = ''
32-
# text_prompt = SimpleActivityPrompt(empty_id, self.validator)
33-
34-
# async def validator(self):
35-
# return True
36-
39+
40+
def test_activity_prompt_with_empty_id_should_fail(self):
41+
empty_id = ''
42+
with self.assertRaises(TypeError):
43+
SimpleActivityPrompt(empty_id, validator)
44+
45+
def test_activity_prompt_with_none_id_should_fail(self):
46+
none_id = None
47+
with self.assertRaises(TypeError):
48+
SimpleActivityPrompt(none_id, validator)
49+
50+
def test_activity_prompt_with_none_validator_should_fail(self):
51+
none_validator = None
52+
with self.assertRaises(TypeError):
53+
SimpleActivityPrompt('EventActivityPrompt', none_validator)
54+
55+
async def test_basic_activity_prompt(self):
56+
async def exec_test(turn_context: TurnContext):
57+
dc = await dialogs.create_context(turn_context)
58+
59+
results = await dc.continue_dialog()
60+
if results.status == DialogTurnStatus.Empty:
61+
options = PromptOptions(prompt=Activity(type=ActivityTypes.message, text='please send an event.'))
62+
await dc.prompt('EventActivityPrompt', options)
63+
elif results.status == DialogTurnStatus.Complete:
64+
await turn_context.send_activity(results.result)
65+
66+
await convo_state.save_changes(turn_context)
67+
68+
# Initialize TestAdapter.
69+
adapter = TestAdapter(exec_test)
70+
71+
# Create ConversationState with MemoryStorage and register the state as middleware.
72+
convo_state = ConversationState(MemoryStorage())
73+
74+
# Create a DialogState property, DialogSet and AttachmentPrompt.
75+
dialog_state = convo_state.create_property('dialog_state')
76+
dialogs = DialogSet(dialog_state)
77+
dialogs.add(SimpleActivityPrompt('EventActivityPrompt', validator))
78+
79+
event_activity = Activity(type=ActivityTypes.event, value=2)
80+
81+
step1 = await adapter.send('hello')
82+
step2 = await step1.assert_reply('please send an event.')
83+
step3 = await step2.send(event_activity)
84+
await step3.assert_reply('2')
85+
86+
async def test_retry_activity_prompt(self):
87+
async def exec_test(turn_context: TurnContext):
88+
dc = await dialogs.create_context(turn_context)
89+
90+
results = await dc.continue_dialog()
91+
if results.status == DialogTurnStatus.Empty:
92+
options = PromptOptions(prompt=Activity(type=ActivityTypes.message, text='please send an event.'))
93+
await dc.prompt('EventActivityPrompt', options)
94+
elif results.status == DialogTurnStatus.Complete:
95+
await turn_context.send_activity(results.result)
96+
97+
await convo_state.save_changes(turn_context)
98+
99+
# Initialize TestAdapter.
100+
adapter = TestAdapter(exec_test)
101+
102+
# Create ConversationState with MemoryStorage and register the state as middleware.
103+
convo_state = ConversationState(MemoryStorage())
104+
105+
# Create a DialogState property, DialogSet and AttachmentPrompt.
106+
dialog_state = convo_state.create_property('dialog_state')
107+
dialogs = DialogSet(dialog_state)
108+
dialogs.add(SimpleActivityPrompt('EventActivityPrompt', validator))
109+
110+
event_activity = Activity(type=ActivityTypes.event, value=2)
111+
112+
step1 = await adapter.send('hello')
113+
step2 = await step1.assert_reply('please send an event.')
114+
step3 = await step2.send('hello again')
115+
step4 = await step3.assert_reply("Please send an 'event'-type Activity with a value of 2.")
116+
step5 = await step4.send(event_activity)
117+
await step5.assert_reply('2')
118+
119+
async def test_activity_prompt_should_return_dialog_end_if_validation_failed(self):
120+
async def exec_test(turn_context: TurnContext):
121+
dc = await dialogs.create_context(turn_context)
122+
123+
results = await dc.continue_dialog()
124+
if results.status == DialogTurnStatus.Empty:
125+
options = PromptOptions(
126+
prompt=Activity(type=ActivityTypes.message, text='please send an event.'),
127+
retry_prompt=Activity(type=ActivityTypes.message, text='event not received.')
128+
)
129+
await dc.prompt('EventActivityPrompt', options)
130+
elif results.status == DialogTurnStatus.Complete:
131+
await turn_context.send_activity(results.result)
132+
133+
await convo_state.save_changes(turn_context)
134+
135+
async def aux_validator(prompt_context: PromptValidatorContext):
136+
assert prompt_context, 'Validator missing prompt_context'
137+
return False
138+
139+
# Initialize TestAdapter.
140+
adapter = TestAdapter(exec_test)
141+
142+
143+
144+
# Create ConversationState with MemoryStorage and register the state as middleware.
145+
convo_state = ConversationState(MemoryStorage())
146+
147+
# Create a DialogState property, DialogSet and AttachmentPrompt.
148+
dialog_state = convo_state.create_property('dialog_state')
149+
dialogs = DialogSet(dialog_state)
150+
dialogs.add(SimpleActivityPrompt('EventActivityPrompt', aux_validator))
151+
152+
step1 = await adapter.send('hello')
153+
step2 = await step1.assert_reply('please send an event.')
154+
step3 = await step2.send('test')
155+
await step3.assert_reply('event not received.')
156+
157+
async def test_activity_prompt_resume_dialog_should_return_dialog_end(self):
158+
async def exec_test(turn_context: TurnContext):
159+
dc = await dialogs.create_context(turn_context)
160+
161+
results = await dc.continue_dialog()
162+
if results.status == DialogTurnStatus.Empty:
163+
options = PromptOptions(prompt=Activity(type=ActivityTypes.message, text='please send an event.'))
164+
await dc.prompt('EventActivityPrompt', options)
165+
166+
second_results = await event_prompt.resume_dialog(dc, DialogReason.NextCalled)
167+
168+
assert second_results.status == DialogTurnStatus.Waiting, 'resume_dialog did not returned Dialog.EndOfTurn'
169+
170+
await convo_state.save_changes(turn_context)
171+
172+
async def aux_validator(prompt_context: PromptValidatorContext):
173+
assert prompt_context, 'Validator missing prompt_context'
174+
return False
175+
176+
# Initialize TestAdapter.
177+
adapter = TestAdapter(exec_test)
178+
179+
# Create ConversationState with MemoryStorage and register the state as middleware.
180+
convo_state = ConversationState(MemoryStorage())
181+
182+
# Create a DialogState property, DialogSet and AttachmentPrompt.
183+
dialog_state = convo_state.create_property('dialog_state')
184+
dialogs = DialogSet(dialog_state)
185+
event_prompt = SimpleActivityPrompt('EventActivityPrompt', aux_validator)
186+
dialogs.add(event_prompt)
187+
188+
step1 = await adapter.send('hello')
189+
step2 = await step1.assert_reply('please send an event.')
190+
await step2.assert_reply('please send an event.')

0 commit comments

Comments
 (0)