adzap-voice_form 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,10 @@
1
+ == 0.3.0 2009-03-22
2
+ * Lots of refactoring for prompts. Prompts can now be a symbol for a component instance method.
3
+ * Added prompt bargein option to set whether the caller can interrupt a prompt
4
+ * Added Simon game example in the examples folder
5
+ * Added as_digits helper
6
+ * Added component start_voice_form class method to start the form without creating component instance first.
7
+
1
8
  == 0.2.0 2009-01-16
2
9
  * Updated to work with Adhearsion 0.8.0 release. Examples updated to match new usage.
3
10
 
@@ -1,4 +1,4 @@
1
- #VoiceForm
1
+ # VoiceForm
2
2
 
3
3
  A plugin for Adhearsion to add form functionality and flow, similar to VoiceXML style forms.
4
4
 
@@ -6,7 +6,7 @@ By Adam Meehan (adam.meehan@gmail.com, [http://duckpunching.com/](http://duckpun
6
6
 
7
7
  Released under the MIT license.
8
8
 
9
- ##Introduction
9
+ ## Introduction
10
10
 
11
11
  After developing VoiceXML (VXML) apps for quite a while and then trying Adhearsion, I found I missed
12
12
  the VXML form element flow when writing components. Given that most interactions with an IVR system
@@ -17,7 +17,7 @@ using XML in a programmatic way, yuck! Also you are not using Ruby, so you miss
17
17
  The plugin attempts to emulate some of the VXML form flow for use in your Adhearsion components.
18
18
 
19
19
 
20
- ##Install
20
+ ## Install
21
21
 
22
22
  sudo gem install adzap-voice_form --source=http://gems.github.com/
23
23
 
@@ -25,7 +25,7 @@ At the bottom your projects startup.rb file put
25
25
 
26
26
  require 'voice_form'
27
27
 
28
- ##Example
28
+ ## Example
29
29
 
30
30
  I use the **speak** command in this example to give better context. The speak command is for TTS
31
31
  and is currently disabled in Adhearsion. In your own application you can just use the **play**
@@ -34,22 +34,27 @@ command to play your sound files.
34
34
  class MyComponent
35
35
  include VoiceForm
36
36
 
37
+ MIN_AGE = 18
38
+
37
39
  voice_form do
40
+ setup do
41
+ # Do stuff here before the form is run
42
+ end
38
43
 
39
44
  field(:age, :max_length => 3, :attempts => 3) do
40
45
  prompt :speak => "Please enter your age", :timeout => 2
41
46
  reprompt :speak => "Enter your age in years", :timeout => 2
42
47
 
43
48
  setup do
44
- @max_age = 110
49
+ # Do stuff here before the field is run
45
50
  end
46
51
 
47
52
  timeout do
48
- call_context.speak "You did not enter anything. Try again."
53
+ call.speak "You did not enter anything. Try again."
49
54
  end
50
55
 
51
56
  validate do
52
- @age.to_i <= @max_age
57
+ @age.to_i >= MIN_AGE
53
58
  end
54
59
 
55
60
  confirm(:accept => 1, :reject => 2, :timeout => 3, :attempts => 3) do
@@ -57,15 +62,15 @@ command to play your sound files.
57
62
  end
58
63
 
59
64
  invalid do
60
- call_context.speak "Your age must be less than #{@max_age}. Try again."
65
+ call.speak "You must be at least #{MIN_AGE} to play. Try again."
61
66
  end
62
67
 
63
68
  success do
64
- call_context.speak "You are #{@age} years old."
69
+ call.speak "You are #{@age} years old."
65
70
  end
66
71
 
67
72
  failure do
68
- call_context.speak "You could not enter your age. Thats a bad sign."
73
+ call.speak "You could not enter your age. Thats a bad sign. You might be too old."
69
74
  end
70
75
  end
71
76
 
@@ -74,6 +79,15 @@ command to play your sound files.
74
79
 
75
80
  In your dialplan:
76
81
 
82
+ For Adhearsion 0.7.999
83
+
84
+ general {
85
+ my_component = new_my_component
86
+ my_component.start_voice_form(self)
87
+ }
88
+
89
+ For Adhearsion 0.8.0
90
+
77
91
  general {
78
92
  MyComponent.new.start_voice_form(self)
79
93
  }
@@ -103,10 +117,6 @@ You can also use `form.restart` to start the form over from the beginning.
103
117
 
104
118
  The form setup block is only run once and is not executed again, even with a `form.restart`.
105
119
 
106
- The `voice_form` method takes only one option
107
-
108
- - :call_context - to nominate the call context method if other than call context
109
-
110
120
 
111
121
  ### field
112
122
 
@@ -115,11 +125,11 @@ in a `voice_form` or on its own inside a component method.
115
125
 
116
126
  The options available are:
117
127
 
118
- - :length - the number of digits to accept
119
- - :min_length - minimum number of digits to accept
120
- - :max_length - maximum number of digits to accept
121
- - :attempts - number of tries to get a valid input
122
- - :call_context - the method name for the call context if other than 'call_context'
128
+ - :length - the number of digits to accept
129
+ - :min_length - minimum number of digits to accept
130
+ - :max_length - maximum number of digits to accept
131
+ - :attempts - number of tries to get a valid input
132
+ - :call - the method name for the call context if other than 'call'. Used for standalone fields not is a form.
123
133
 
124
134
  All fields defined get an accessor method defined of the same name in the component class.
125
135
  This means you can access its value using the instance variable or the accessor method inside any of
@@ -131,8 +141,9 @@ hash of options to control the prompt such as:
131
141
 
132
142
  - :play - play one or more sound files
133
143
  - :speak - play TTS text (needs my Adhearsion hack for speak in input command)
134
- - :timeout - number of seconds to wait for input
144
+ - :timeout - number of seconds to wait for input. Default is 5.
135
145
  - :repeats - number of attempts to use this prompt until the next one is used
146
+ - :bargein - whether to allow caller to interrupt prompt. Default is true.
136
147
 
137
148
  The length expected for the input is taken from the options passed to the `field` method.
138
149
 
@@ -160,26 +171,26 @@ instance variables and component methods are available to use including the call
160
171
 
161
172
  The details of each callback are as follows
162
173
 
163
- ### setup
174
+ #### setup
164
175
 
165
176
  This is run once only for a field if defined before any prompts
166
177
 
167
- ### timeout
178
+ #### timeout
168
179
 
169
- This is run if no input is received.
180
+ This is run if no input is recevied or input is not of a valid length as defined by length or min_length
181
+ field options.
170
182
 
171
- ### validate
183
+ #### validate
172
184
 
173
185
  This is run after input of a valid length. The validate block is where you put validation logic of the
174
186
  value just input by the user. The block should return `true` if the value is valid or `false` otherwise.
175
187
  If the validate callback returns false then the invalid callback will be called next.
176
188
 
177
- ### invalid
189
+ #### invalid
178
190
 
179
- The invalid callback is called if the input value is not of a valid length or the validate block returns
180
- false.
191
+ The invalid callback is called if validate block returns false.
181
192
 
182
- ### confirm
193
+ #### confirm
183
194
 
184
195
  The confirm callback is called after the input has been validated. The confirm callback is a little different
185
196
  from the others. Idea is that you return either an array or string of the audio files or TTS text, respectively,
@@ -199,7 +210,7 @@ For example, in a field called my_field:
199
210
  ['you-entered', @my_field.scan(/\d/), 'is-this-correct', 'press-1-accept-2-try-again'].flatten
200
211
  end
201
212
 
202
- The above will `play` the array of audo files as the prompt for confirmation.
213
+ The above will `play` the array of audio files as the prompt for confirmation.
203
214
 
204
215
  confirm(:accept => 1, :reject => 2, :attempts => 3) do
205
216
  "You entered #{@my_field}. Is this correct? Press 1 to accept or 2 try again."
@@ -207,12 +218,43 @@ The above will `play` the array of audo files as the prompt for confirmation.
207
218
 
208
219
  The above will `speak` the string as the prompt for confirmation.
209
220
 
210
- If no valid input is entered for the confirmation
221
+ If no valid input is entered for the confirmation then another you will be reprompted to enter the field value.
222
+
223
+
224
+ ### Form methods
225
+
226
+ Inside a callback you have the `form` method available. The returns the instance of the current form. The form
227
+ has some methods to allow you to perform form actions which manipulate the form stack. These actions are as follows:
228
+
229
+ #### form.goto
230
+
231
+ Inside any callback you can use the `goto` command to designate which field the form should run after the
232
+ current field. Normally the form will progress through the fields in the order defined, but a goto with shift
233
+ the current form position to the field name pass to it like so:
234
+
235
+ failure do
236
+ form.goto :other_field_name
237
+ end
238
+
239
+ The form continues from the field in the goto run each subsequent field in order. If the goto field is above the
240
+ current field then the current field will be executed again when it is reached in the stack. If the goto field
241
+ is below the current field then form will continue there, skipping whatever fields may lie between the current
242
+ and the goto field.
243
+
244
+
245
+ #### form.restart
246
+
247
+ The form may be restarted from the start at any point with `form.restart`. This will go back to the top of the
248
+ form and proceed through each field again. The form setup will not be run again however.
249
+
250
+
251
+ #### form.exit
211
252
 
212
- TODO: More docs
253
+ To exit the form after the current field is complet just execute `form.exit`. The application will then be
254
+ returned to where the form was started, be it a dialplan or another form.
213
255
 
214
256
 
215
- ##Credits
257
+ ## Credits
216
258
 
217
259
  Adam Meehan (adam.meehan@gmail.com, [http://duckpunching.com/](http://duckpunching.com/))
218
260
 
data/Rakefile CHANGED
@@ -5,7 +5,7 @@ require 'date'
5
5
  require 'spec/rake/spectask'
6
6
 
7
7
  GEM = "voice_form"
8
- GEM_VERSION = "0.2.0"
8
+ GEM_VERSION = "0.3.0"
9
9
  AUTHOR = "Adam Meehan"
10
10
  EMAIL = "adam.meehan@gmail.com"
11
11
  HOMEPAGE = "http://github.com/adzap/voice_form"
@@ -1,19 +1,23 @@
1
1
  class MyComponent
2
2
  include VoiceForm
3
3
 
4
- delegate :play, :speak, :to => :call_context
4
+ MAX_AGE = 110
5
+
6
+ delegate :play, :speak, :to => :call
5
7
 
6
8
  voice_form do
7
9
  field(:age, :max_length => 3, :attempts => 3) do
8
10
  prompt :speak => "Please enter your age", :timeout => 2, :repeats => 2
9
11
  reprompt :speak => "Enter your age in years", :timeout => 2
10
-
11
- setup { @max_age = 110 }
12
-
13
- validate { @age.to_i < @max_age }
14
-
12
+
13
+ confirm do
14
+ "Are you sure you are #{@age} years old? Press 1 to confirm, or 2 to retry."
15
+ end
16
+
17
+ validate { @age.to_i < MAX_AGE }
18
+
15
19
  invalid do
16
- speak "Your age must be less than #{@max_age}. Try again."
20
+ speak "You cannot be that old. Try again."
17
21
  end
18
22
 
19
23
  success do
@@ -30,7 +34,7 @@ class MyComponent
30
34
  end
31
35
 
32
36
  field(:postcode, :length => 4, :attempts => 5) do
33
- prompt :speak => "Please enter your 4 digit post code", :timeout => 3
37
+ prompt :speak => "Please enter your 4 digit postcode", :timeout => 3
34
38
 
35
39
  validate { @postcode[0..0] != '0' }
36
40
 
@@ -0,0 +1,47 @@
1
+ methods_for :dialplan do
2
+ def simon_game_voice_form
3
+ SimonGameVoiceForm.start_voice_form(self)
4
+ end
5
+ end
6
+
7
+ class SimonGameVoiceForm
8
+ include VoiceForm
9
+
10
+ voice_form do
11
+ setup do
12
+ @number = ''
13
+ end
14
+
15
+ field(:attempt, :attempts => 1) do
16
+ prompt :play => :current_number, :bargein => false, :timeout => 2
17
+
18
+ setup do
19
+ @number << random_number
20
+ end
21
+
22
+ validate do
23
+ @attempt == @number
24
+ end
25
+
26
+ success do
27
+ call.play 'good'
28
+ form.restart
29
+ end
30
+
31
+ failure do
32
+ call.play %W[#{@number.length-1} times wrong-try-again-smarty]
33
+ @number = ''
34
+ form.restart
35
+ end
36
+ end
37
+
38
+ end
39
+
40
+ def random_number
41
+ rand(10).to_s
42
+ end
43
+
44
+ def current_number
45
+ as_digits(@number)
46
+ end
47
+ end
@@ -10,7 +10,9 @@ module VoiceForm
10
10
  @options = options
11
11
  @form_stack = []
12
12
  @stack_index = 0
13
- self.instance_eval(&block)
13
+
14
+ instance_eval(&block)
15
+ raise 'A form requires at least one field defined' if fields.empty?
14
16
  end
15
17
 
16
18
  def run(component)
@@ -30,10 +32,7 @@ module VoiceForm
30
32
  end
31
33
 
32
34
  def goto(name)
33
- index = nil
34
- form_stack.each_with_index {|slot, i|
35
- index = i and break if form_field?(slot) && slot.name == name
36
- }
35
+ index = field_index(name)
37
36
  raise "goto failed: No form field found with name '#{name}'." unless index
38
37
  @stack_index = index
39
38
  end
@@ -43,7 +42,7 @@ module VoiceForm
43
42
  end
44
43
 
45
44
  def exit
46
- @exit_form = true
45
+ @exit = true
47
46
  end
48
47
 
49
48
  private
@@ -53,7 +52,7 @@ module VoiceForm
53
52
  end
54
53
 
55
54
  def run_form_stack
56
- while @stack_index < form_stack.size do
55
+ while @stack_index < form_stack.size && !@exit do
57
56
  slot = form_stack[@stack_index]
58
57
  @stack_index += 1
59
58
 
@@ -64,8 +63,6 @@ module VoiceForm
64
63
  @current_field = nil
65
64
  @component.instance_eval(&slot)
66
65
  end
67
-
68
- break if @exit_form
69
66
  end
70
67
  @stack_index = 0
71
68
  @current_field = nil
@@ -73,11 +70,10 @@ module VoiceForm
73
70
 
74
71
  def add_field_accessors
75
72
  return if @accessors_added
76
-
77
- form_stack.each do |field|
78
- next unless form_field?(field)
73
+
74
+ fields.keys.each do |field_name|
79
75
  @component.class.class_eval do
80
- attr_accessor field.name
76
+ attr_accessor field_name
81
77
  end
82
78
  end
83
79
 
@@ -87,7 +83,19 @@ module VoiceForm
87
83
  def form_field?(slot)
88
84
  slot.is_a?(VoiceForm::FormField)
89
85
  end
90
-
86
+
87
+ def fields
88
+ @fields ||= form_stack.inject({}) do |flds,s|
89
+ flds[s.name] = s if form_field?(s)
90
+ flds
91
+ end
92
+ end
93
+
94
+ def field_index(field)
95
+ form_stack.each_with_index {|slot, i|
96
+ return i if form_field?(slot) && slot.name == field.to_sym
97
+ }
98
+ end
91
99
  end
92
100
 
93
101
  end
@@ -1,24 +1,28 @@
1
1
  module VoiceForm
2
2
 
3
3
  class FormField
4
+ cattr_accessor :default_prompt_options
4
5
  attr_reader :name
5
- attr_accessor :prompts
6
+
7
+ self.default_prompt_options = { :bargein => true, :timeout => 5 }
6
8
 
7
9
  def initialize(name, options, component, &block)
8
10
  @name, @options, @component = name, options, component
9
- @options.reverse_merge!(:attempts => 5, :call_context => 'call_context')
11
+ @options.reverse_merge!(:attempts => 5, :call => 'call')
10
12
  @callbacks = {}
11
- @prompts = []
12
- self.instance_eval(&block)
13
+ @prompt_queue = []
14
+
15
+ instance_eval(&block)
16
+ raise 'A field requires a prompt to be defined' if @prompt_queue.empty?
13
17
  end
14
18
 
15
19
  def prompt(options)
16
- add_prompts(options.reverse_merge(:timeout => 5))
20
+ add_prompt(options.reverse_merge(self.class.default_prompt_options))
17
21
  end
18
22
 
19
23
  def reprompt(options)
20
- raise 'A reprompt can only be used after a prompt' if @prompts.empty?
21
- add_prompts(options.reverse_merge(:timeout => 5))
24
+ raise 'A reprompt can only be used after a prompt' if @prompt_queue.empty?
25
+ add_prompt(options.reverse_merge(self.class.default_prompt_options))
22
26
  end
23
27
 
24
28
  def setup(&block)
@@ -47,29 +51,30 @@ module VoiceForm
47
51
 
48
52
  def confirm(options={}, &block)
49
53
  options.reverse_merge!(
50
- :attempts => 3,
51
- :accept => 1,
52
- :reject => 2,
53
- :timeout => 3
54
+ self.class.default_prompt_options.merge(
55
+ :attempts => 3,
56
+ :accept => 1,
57
+ :reject => 2
58
+ )
54
59
  )
55
- @confirmation_options = options.merge(:block => block)
60
+ @confirmation_options = options.merge(:message => block)
56
61
  end
57
62
 
58
63
  def run(component=nil)
59
64
  @component = component if component
60
65
 
61
- set_component_value('')
62
-
63
66
  run_callback(:setup)
64
67
 
65
68
  result = 1.upto(@options[:attempts]) do |attempt|
66
- if get_input(attempt).empty?
69
+ prompt = prompt_for_attempt(attempt)
70
+
71
+ @value = get_input(prompt)
72
+
73
+ unless valid_length?
67
74
  run_callback(:timeout)
68
75
  next
69
76
  end
70
77
 
71
- set_component_value @value
72
-
73
78
  if input_valid?
74
79
  if value_confirmed?
75
80
  break 0
@@ -89,48 +94,43 @@ module VoiceForm
89
94
 
90
95
  private
91
96
 
92
- def prompt_for_attempt(attempt)
93
- prompt = if attempt == 1 || @prompts.size == 1 then
94
- @prompts.first
97
+ def get_input(prompt)
98
+ method = prompt[:method]
99
+ message = prompt[:message]
100
+
101
+ if prompt[:bargein]
102
+ prompt[method] = message
95
103
  else
96
- @prompts[attempt-1] || @prompts.last
104
+ call.send(method, message)
97
105
  end
98
- evaluate_prompt(prompt)
99
- end
100
106
 
101
- def get_input(attempt)
102
- input_options = @options.dup
103
- input_options.merge!(prompt_for_attempt(attempt))
104
- args = [ input_options ]
105
- length = input_options.delete(:length) || input_options.delete(:max_length)
106
- args.unshift(length) if length
107
- @value = call_context.input(*args)
107
+ args = [ prompt.slice(method, :timeout, :accept_key) ]
108
+ args.unshift(prompt[:length]) if prompt[:length]
109
+ call.input(*args)
108
110
  end
109
111
 
110
112
  def input_valid?
111
- @value.size >= minimum_length &&
112
- @value.size <= maximum_length &&
113
- run_callback(:validate)
113
+ run_callback(:validate)
114
+ end
115
+
116
+ def valid_length?
117
+ !@value.empty? &&
118
+ @value.size >= minimum_length &&
119
+ @value.size <= maximum_length
114
120
  end
115
121
 
116
122
  def value_confirmed?
117
123
  return true unless @confirmation_options
118
- options = @confirmation_options.dup
119
124
 
120
- if block = options.delete(:block)
121
- message = @component.instance_eval(&block)
122
- prompt = case message
123
- when Array: {:play => message}
124
- when String: {:speak => message}
125
- end
126
- prompt.merge!(options.slice(:timeout))
127
- end
128
- 1.upto(options[:attempts]) do |attempt|
129
- value = call_context.input(1, prompt)
130
- case value
131
- when options[:accept].to_s
125
+ prompt = evaluate_prompt(@confirmation_options)
126
+ prompt[:method] = prompt[:message].is_a?(Array) ? :play : :speak
127
+ prompt[:length] = [ prompt[:accept].to_s.size, prompt[:reject].to_s.size ].max
128
+
129
+ 1.upto(prompt[:attempts]) do |attempt|
130
+ case get_input(prompt)
131
+ when prompt[:accept].to_s
132
132
  return true
133
- when options[:reject].to_s
133
+ when prompt[:reject].to_s
134
134
  return false
135
135
  else
136
136
  next
@@ -140,6 +140,7 @@ module VoiceForm
140
140
  end
141
141
 
142
142
  def run_callback(callback)
143
+ set_component_value @value
143
144
  if block = @callbacks[callback]
144
145
  result = @component.instance_eval(&block)
145
146
  @value = get_component_value
@@ -154,7 +155,7 @@ module VoiceForm
154
155
  end
155
156
 
156
157
  def get_component_value
157
- @component.send("#{@name}")
158
+ @component.send(@name)
158
159
  end
159
160
 
160
161
  def minimum_length
@@ -165,20 +166,44 @@ module VoiceForm
165
166
  @options[:max_length] || @options[:length] || @value.size
166
167
  end
167
168
 
168
- def call_context
169
- @call_context ||= @component.send(@options[:call_context])
169
+ def call
170
+ @call ||= @component.send(@options[:call])
170
171
  end
171
172
 
172
- def add_prompts(options)
173
+ def add_prompt(options)
174
+ method = options.has_key?(:play) ? :play : :speak
175
+ options[:message] = options.delete(method)
176
+ options[:method] = method
177
+ options[:length] = @options[:length] || @options[:max_length]
178
+
173
179
  repeats = options[:repeats] || 1
174
- @prompts += ([options] * repeats)
180
+ @prompt_queue += ([options] * repeats)
181
+ end
182
+
183
+ def prompt_for_attempt(attempt)
184
+ prompt = if attempt == 1 || @prompt_queue.size == 1 then
185
+ @prompt_queue.first
186
+ else
187
+ @prompt_queue[attempt-1] || @prompt_queue.last
188
+ end
189
+ evaluate_prompt(prompt)
175
190
  end
176
191
 
177
192
  def evaluate_prompt(prompt)
178
- key = prompt.has_key?(:play) ? :play : :speak
179
- message = prompt[key]
180
- message = @component.instance_eval(&message) if message.is_a?(Proc)
181
- prompt.merge(key => message)
193
+ options = prompt.dup
194
+ message = options[:message]
195
+
196
+ message = case message
197
+ when String, Array
198
+ message
199
+ when Symbol
200
+ @component.send(message)
201
+ when Proc
202
+ @component.instance_eval(&message)
203
+ end
204
+
205
+ options[:message] = message
206
+ options
182
207
  end
183
208
  end
184
209