bot_framework 0.1.0beta → 0.1.0beta2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +4 -1
  3. data/README.md +14 -1
  4. data/appveyor.yml +20 -0
  5. data/bot_framework.gemspec +6 -0
  6. data/examples/dialog/basic_first_run.rb +12 -0
  7. data/examples/ruby_conf/Gemfile +8 -0
  8. data/examples/ruby_conf/Gemfile.lock +85 -0
  9. data/examples/ruby_conf/bot.rb +88 -0
  10. data/examples/ruby_conf/config.ru +6 -0
  11. data/examples/ruby_conf/speaker_data.yaml +41 -0
  12. data/examples/simple_regex/Gemfile +6 -0
  13. data/examples/simple_regex/Gemfile.lock +63 -0
  14. data/examples/simple_regex/bot.rb +35 -0
  15. data/examples/simple_regex/config.ru +6 -0
  16. data/images/emulator1.png +0 -0
  17. data/images/emulator2.png +0 -0
  18. data/lib/bot_framework.rb +12 -0
  19. data/lib/bot_framework/api_base.rb +6 -4
  20. data/lib/bot_framework/bot.rb +47 -2
  21. data/lib/bot_framework/connector.rb +13 -5
  22. data/lib/bot_framework/console_connector.rb +17 -0
  23. data/lib/bot_framework/dialogs/action_set.rb +66 -0
  24. data/lib/bot_framework/dialogs/dialog.rb +25 -0
  25. data/lib/bot_framework/dialogs/entity_recognizer.rb +123 -0
  26. data/lib/bot_framework/dialogs/luis_recognizer.rb +72 -0
  27. data/lib/bot_framework/dialogs/reg_exp_recognizer.rb +41 -0
  28. data/lib/bot_framework/dialogs/simple_dialog.rb +29 -0
  29. data/lib/bot_framework/events/event_emitter.rb +6 -0
  30. data/lib/bot_framework/message.rb +80 -0
  31. data/lib/bot_framework/models/object.rb +0 -1
  32. data/lib/bot_framework/prompt.rb +62 -0
  33. data/lib/bot_framework/server.rb +1 -1
  34. data/lib/bot_framework/session.rb +407 -0
  35. data/lib/bot_framework/simple_prompt_recognizer.rb +4 -0
  36. data/lib/bot_framework/token_validator.rb +6 -4
  37. data/lib/bot_framework/universal_bot.rb +7 -0
  38. data/lib/bot_framework/version.rb +1 -1
  39. metadata +100 -4
@@ -0,0 +1,41 @@
1
+ require_relative 'action_set'
2
+ module BotFramework
3
+ module Dialogs
4
+ class RegExpRecognizer
5
+ attr_accessor :intent, :expressions
6
+ def initialize(intent,expressions)
7
+ @intent = intent
8
+ if expressions.is_a? Regexp
9
+ @expressions = {'*': expressions}
10
+ else
11
+ @expressions = expressions || {}
12
+ end
13
+ end
14
+
15
+ def recognize(context)
16
+ raise ArgumentError, "context must be a hash" unless context.is_a? Hash
17
+ result = {score: 0.0, intent: nil}
18
+ if context.fetch(:message, {}).fetch(:text, nil)
19
+ utterance = context[:message][:text]
20
+ locale = context[:message][:locale] || :*
21
+ exp = @expressions[locale] ? @expressions[locale] : @expressions[:*]
22
+ if exp
23
+ matches = exp.match(utterance)
24
+ if matches
25
+ matched = matches.to_s
26
+ result[:score] = matched.length / utterance.length
27
+ result[:intent] = intent
28
+ result[:expression] = exp
29
+ result[:matched] = matches
30
+ end
31
+ yield nil, result
32
+ else
33
+ yield(ExpressionNotFoundForLocale, nil)
34
+ end
35
+ else
36
+ yield(nil, result)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,29 @@
1
+ require_relative '../session'
2
+ require_relative 'action_set'
3
+ module BotFramework
4
+ # Abstract class for dialog
5
+ module Dialogs
6
+ class SimpleDialog < Dialog
7
+ def initialize
8
+ raise 'No block given' unless block_given?
9
+ super
10
+ end
11
+
12
+ def begin(session, _opts = {})
13
+ reply_recieved(session)
14
+ end
15
+
16
+ def reply_recieved(_session, _recognize_result = {})
17
+ raise NotImplementedError
18
+ end
19
+
20
+ def dialog_resumed(session, result)
21
+ session.error(result[:error]) if result[:error]
22
+ end
23
+
24
+ def recognize(_context)
25
+ yield nil, { score: 0.1 }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,6 @@
1
+ module BotFramework
2
+ module Events
3
+ class EventEmitter
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,80 @@
1
+ module BotFramework
2
+ class Message
3
+ TEXT_FORMAT = {
4
+ plain: 'plain',
5
+ markdown: 'markdown',
6
+ xml: 'xml'
7
+ }.freeze
8
+ ATTACHMENT_LAYOUT = {
9
+ list: 'list',
10
+ carousel: 'carousel'
11
+ }.freeze
12
+
13
+ def initialize(_session = nil)
14
+ @data = {}
15
+ @data[:type] = 'consts.MessageType' # FIXME
16
+ @data[:agent] = 'consts.agent'
17
+ if @session
18
+ m = @session.message
19
+ @data[:source] = m[:source] if m[:source]
20
+ @data[:text_locale] = m[:text_locale] if m[:text_locale]
21
+ @data[:address] = m[:address] if m[:address]
22
+ end
23
+ end
24
+
25
+ def text_locale(locale)
26
+ @data[:text_locale] = locale
27
+ self
28
+ end
29
+
30
+ def text_format(style)
31
+ @data[:text_format] = style
32
+ self
33
+ end
34
+
35
+ def text(text, *_args)
36
+ @data[:text] = text.present? ? format_text(text) : ''
37
+ self
38
+ end
39
+
40
+ def ntext(msg, _msg_plural, count)
41
+ fmt = count == 1 ? self.class.random_prompt(msg) : self.class.random_prmpt(message_plural)
42
+ fmt = @session.get_text(fmt) if @session
43
+ @data[:text] = fmt, count # FIXME
44
+ self
45
+ end
46
+
47
+ def compose(prompts, *args)
48
+ if prompts
49
+ @data[:text] = Message.compose_prompt(@session, prompts, *args)
50
+ self
51
+ end
52
+ end
53
+
54
+ def summary; end
55
+
56
+ def attachment_layout; end
57
+
58
+ def attachments; end
59
+
60
+ def add_attachment; end
61
+
62
+ def entities; end
63
+
64
+ def add_entity; end
65
+
66
+ def address; end
67
+
68
+ def timestamp; end
69
+
70
+ def source_event; end
71
+
72
+ def to_message; end
73
+
74
+ class << self
75
+ def random_prompt; end
76
+
77
+ def compose_prompt; end
78
+ end
79
+ end
80
+ end
@@ -1,6 +1,5 @@
1
1
  module BotFramework
2
2
  class Object < Base
3
-
4
3
  # Attribute type mapping.
5
4
  def self.swagger_types
6
5
  {
@@ -0,0 +1,62 @@
1
+ module BotFramework
2
+ class Prompt < Dialog
3
+ @@options = {
4
+ recognizer: BotFramework::SimplePromptRecognizer.new,
5
+ prompt_after_action: true
6
+ }
7
+
8
+ @@default_retry_prompt = {
9
+ text: 'default_text',
10
+ number: 'default_number',
11
+ confirm: 'daefault_confirm',
12
+ choice: 'default_choice',
13
+ time: 'default_time',
14
+ attachment: 'default_file'
15
+ }
16
+
17
+ def self.configure(_options)
18
+ end
19
+
20
+ def begin(session, options = {})
21
+ options[:prompt_after_action] = options[:prompt_after_action] || options[:prompt_after_action]
22
+ options[:retry_count] = 0
23
+ options.each do |option|
24
+ # Store in dialog data
25
+ end
26
+ send_prompt(session, options)
27
+ end
28
+
29
+ def reply_received(session, args = {}); end
30
+
31
+ def dialog_resumed; end
32
+
33
+ def recognize; end
34
+
35
+ def send_prompt(session, options); end
36
+
37
+ def create_prompt; end
38
+
39
+ def self.text
40
+ end
41
+
42
+ def self.number
43
+ end
44
+
45
+ def self.confirm
46
+ end
47
+
48
+ def self.choice
49
+ end
50
+
51
+ def self.time
52
+ end
53
+
54
+ def self.attachment
55
+ end
56
+
57
+ def self.disambiguate
58
+ end
59
+
60
+ private
61
+ end
62
+ end
@@ -40,7 +40,7 @@ module BotFramework
40
40
  if validator.valid?
41
41
  return true
42
42
  else
43
- p "Errors: #{validator.errors}"
43
+ BotFramework.logger.error "Errors: #{validator.errors}"
44
44
  return false
45
45
  end
46
46
  rescue JWT::DecodeError
@@ -0,0 +1,407 @@
1
+ module BotFramework
2
+ class Session # < Events::EventEmitter
3
+ attr_accessor :library, :message, :user_data, :conversation_data, :private_conversation_data,
4
+ :session_state, :dialog_state, :localizer
5
+ def initialize(options)
6
+ super
7
+ @library = options[:library]
8
+ @localizer = options[:localizer]
9
+ @msg_sent = false
10
+ @is_reset = false
11
+ @last_send_time = Time.now # TODO: Move to proc?
12
+ @batch = []
13
+ @batch_timer = Timers::Group.new
14
+ @batch_started = false
15
+ @sending_batch = false
16
+ @in_middleware = false
17
+ @_locale = nil
18
+
19
+ @options = options || {
20
+ on_save: nil,
21
+ on_send: nil,
22
+ library: nil,
23
+ localizer: nil,
24
+ middleware: nil,
25
+ dialog_id: nil,
26
+ dialog_args: nil,
27
+ auto_batch_delay: nil,
28
+ dialog_error_message: nil,
29
+ actions: nil
30
+ }
31
+ @auto_batch_delay = 250 unless options[:auto_batch_delay].is_a? Integer
32
+ @timers = Timers::Group.new
33
+ end
34
+
35
+ def to_recognize_context
36
+ {
37
+ message: message,
38
+ user_data: user_data,
39
+ conversation_data: conversation_data,
40
+ private_conversation_data: private_conversation_data,
41
+ localizer: localizer,
42
+ dialog_stack: dialog_stack,
43
+ preferred_locale: preferred_locale,
44
+ get_text: get_text,
45
+ nget_text: nget_text,
46
+ locale: preferred_locale
47
+ }
48
+ end
49
+
50
+ def dispatch(session_state, message)
51
+ index = 0
52
+ session = self
53
+ now = Time.now
54
+ middleware = @options[:middleware] || []
55
+
56
+ _next = lambda do
57
+ handler = middleware[index] if index < middleware.length
58
+ if handler
59
+ index += 1
60
+ handler(session, _next)
61
+ else
62
+ @in_middleware = false
63
+ @session_state[:last_acess] = now
64
+ done
65
+ end
66
+ end
67
+ session_state ||= { call_stack: [], last_acess: Time.now, version: 0.0 }
68
+ # Making sure that dialog is properly initialized
69
+ cur = cur_dialog
70
+ self.dialog_data = cur.state if cur
71
+ # Dispatch message
72
+ message ||= { text: '' }
73
+ message[:type] ||= 'message'
74
+ # Ensure that localized prompts are loaded
75
+ # TODO
76
+ self
77
+ end
78
+
79
+ def error(_error)
80
+ logger.info 'Session error'
81
+ if options[:dialog_error_message]
82
+ end_conversation(options[:dialog_error_message])
83
+ else
84
+ # TODO: Add localisation
85
+ locale = preferred_locale
86
+ end_conversation 'Error in conversation'
87
+ end
88
+
89
+ # TODO: Log error
90
+ end
91
+
92
+ def preferred_locale(locale = nil)
93
+ if locale
94
+ @_locale = locale
95
+ @user_data['BotBuilder.Data.PreferredLocale'] = locale if @user_data
96
+ @localizer.load if @localizer # TODO
97
+ elsif !@_locale
98
+ if @user_data && @user_data['BotBuilder.Data.PreferredLocale']
99
+ @_locale = @user_data['BotBuilder.Data.PreferredLocale']
100
+ elsif @message && @message[:text_locale]
101
+ @_locale = @message[:text_locale]
102
+ elsif @localizer
103
+ @_locale = @localizer[:default_locale]
104
+ end
105
+ end
106
+ @_locale
107
+ end
108
+
109
+ # Gets and formats localized text string
110
+ def gettext(message_id, options = {})
111
+ # TODO
112
+ # stub
113
+ end
114
+
115
+ # Gets and formats the singular/plural form of a localized text string.
116
+ def ngettext(message_id, message_id_plural, count); end
117
+
118
+ # Manually save current session state
119
+ def save
120
+ logger.info 'Session.save'
121
+ start_batch
122
+ self
123
+ end
124
+
125
+ def send(message, args = [])
126
+ args.unshift(@cur_library_name, message)
127
+ send_localized(args, message)
128
+ end
129
+
130
+ def send_localized(_localization_namspace, message, _args = [])
131
+ # TODO: Incomplete
132
+ @msg_sent = true
133
+ m = { text: message }
134
+ prepare_message(m)
135
+ @batch << m
136
+ self
137
+ end
138
+
139
+ # Sends a typing indicator to the user
140
+ def send_typing
141
+ @msg_sent = true
142
+ m = { type: 'typing' }
143
+ m = prepare_message(m)
144
+ @batch.push(m)
145
+ logger.info 'Send Typing'
146
+ send_batch
147
+ end
148
+
149
+ def send_batch
150
+ logger.info "Sending batch with elements #{@batch.count}"
151
+ return if sending_batch?
152
+ # TODO: timer
153
+ @batch_timer = nil
154
+ batch = @batch
155
+ @batch_started = false
156
+ @sending_batch = true
157
+ cur = cur_dialog
158
+ cur.state = @dialog_data
159
+ options[:on_save] = proc do |err|
160
+ if err
161
+ @sending_batch = false
162
+ case (err.code || '')
163
+ when 'EBADMSG'
164
+ when 'EMSGSIZE'
165
+ # Something went wrong , let's reset everything
166
+ @user_data = {}
167
+ @batch = []
168
+ end_conversation(@options[:dialog_error_message] || 'Something went wrong and we need to startover')
169
+ end
170
+ yield(err) if block_given?
171
+ else
172
+ if batch.length
173
+ options[:on_send].call(batch) do |err|
174
+ @sending_batch = false
175
+ start_batch if @batch_started # What is this :o
176
+ yield(err) if block_given?
177
+ end
178
+ else
179
+ @sending_batch = false
180
+ start_batch if @batch_started
181
+ yield(err) if block_given?
182
+ end
183
+ end
184
+ end
185
+ end
186
+
187
+ # Begin a new dialog
188
+ def begin_dialog(id, args = nil)
189
+ logger.info "Beginning new dialog #{id}"
190
+ id = resolve_dialog_id(id)
191
+ dialog = find_dialog(id)
192
+ raise "Dialog #{id} Not found" unless dialog
193
+ push_dialog(id: id, state: {})
194
+ start_batch
195
+ dialog.begin(self, args)
196
+ self
197
+ end
198
+
199
+ def replace_dialog(id, args = nil)
200
+ logger.info "Session replace dialog #{id}"
201
+ id = resolve_dialog_id(id)
202
+ dialog = find_dialog(id)
203
+ raise "Dialog #{id} Not found" unless dialog
204
+
205
+ # Update the stack and start dialog
206
+ pop_dialog
207
+ push_dialog(id: id, state: {})
208
+ start_batch
209
+ dialog.begin(self, args)
210
+ self
211
+ end
212
+
213
+ # End conversation with the user
214
+ def end_conversation(message = nil, _args = {})
215
+ if message
216
+ # TODO: sent message
217
+ end
218
+ # Clear private conversation data
219
+ @private_conversation_data = {}
220
+
221
+ # Clear stack and save
222
+ logger.info 'End conversation'
223
+ ss = @session_state
224
+ ss.call_stack = []
225
+ send_batch
226
+ self
227
+ end
228
+
229
+ def end_dialog(message = nil, args = {})
230
+ if message
231
+ # TODO: end_dialog_with_result
232
+ end
233
+ cur = cur_dialog
234
+ if cur
235
+ if message
236
+ m = if (message.is_a? String) || (message.is_a? Array)
237
+ create_message(cur_library_name, message, args)
238
+ else
239
+ message
240
+ end
241
+ @msg_sent = true
242
+ prepare_message(m)
243
+ @batch.push(m)
244
+ end
245
+
246
+ # Pop the dialog of the stack and resume the parent
247
+ logger.info 'Session.end_dialog'
248
+ child_id = cur.id
249
+ cur = pop_dialog
250
+ start_batch
251
+ if cur
252
+ dialog = find_dialog(cur.id)
253
+ if dialog
254
+ dialog.dialog_resumed(self, resumed: :completed, response: true, child_id: child_id)
255
+ else
256
+ # Bad dialog !! , we should never reach here
257
+ raise "Can't resume , missing parent"
258
+ end
259
+ end
260
+ end
261
+ end
262
+
263
+ # Ends current dialog and returns a value to the caller
264
+ def end_dialog_with_result(result)
265
+ # Validate call stack
266
+ cur = cur_dialog
267
+ if cur
268
+ # Validate the result
269
+ result ||= {}
270
+ result[:resumed] ||= :completed
271
+ result.child_id = cur.id
272
+ logger.info 'Session.end_dialog_with_result'
273
+
274
+ # Pop dialog of the stack and resume parent dialog
275
+ cur = pop_dialog
276
+ start_batch
277
+ if cur
278
+ dialog = find_dialog(cur.id)
279
+ if dialog
280
+ dialog.dialog_resumed(self, result)
281
+ else
282
+ # Bad dialog state
283
+ raise "Can't resume , missing parent dialog"
284
+ end
285
+ end
286
+ end
287
+ self
288
+ end
289
+
290
+ def cancel_dialog; end
291
+
292
+ def reset; end
293
+
294
+ ############### Dialog Stack Management ############################33
295
+
296
+ # Gets and Sets current dialog stack
297
+ def dialog_stack(new_stack = nil)
298
+ if new_stack
299
+ # Update stack and dialog data
300
+
301
+ stack = @session_state[:call_stack] = new_stack || []
302
+ @dialog_data = stack.empty? nil || stack.last
303
+ else
304
+ # Copy over dialog data to slack
305
+ stack = @session_state[:call_stack] || []
306
+ stack.last.state = @dialog_data || {}
307
+ end
308
+ stack
309
+ end
310
+
311
+ # Clears the current Dialog stack
312
+ def clear_dialog_stack
313
+ @session_state[:call_stack] = []
314
+ @dialog_data = nil
315
+ end
316
+
317
+ # Enumerates all a stacks dialog entries in either a forward or reverse direction.
318
+ def self.forEachDialogStackEntry(stack)
319
+ stack.each { |item| yield(item) }
320
+ end
321
+
322
+ # Searches a dialog stack for a specific dialog, in either a forward or reverse direction, returning its index.
323
+ def self.findEachDialogStackEntry(stack, dialog_id)
324
+ stack.each_with_index do |item, index|
325
+ return index if item[:id] = dialog_id
326
+ end
327
+ -1
328
+ end
329
+
330
+ # Returns a active stack entry or nil
331
+ def self.active_dialog_stack_entry(stack)
332
+ stack.last || nil
333
+ end
334
+
335
+ # Pushes a new dialog into stack and return it as active dialog
336
+ def self.push_dialog_stack_entry(statck, entry)
337
+ entry[:state] ||= {}
338
+ statck ||= []
339
+ stack.push(entry)
340
+ entry
341
+ end
342
+
343
+ # Pop active dialog out of the stack
344
+ def self.pop_dialog_stack_entry(stack)
345
+ stack.pop if stack
346
+ Session.active_dialog_stack_entry(stack)
347
+ end
348
+
349
+ # Deletes all dialog stack entries starting with the specified index and returns the new active dialog.
350
+ def self.prune_dialog_stack(_stack, _start)
351
+ end
352
+
353
+ # Ensures that all of the entries on a dialog stack reference valid dialogs within a library hierarchy.
354
+ def self.validate_dialog_stack(_stack, _root)
355
+ end
356
+
357
+ ## Message routing
358
+ #########################
359
+
360
+ # Dispatches handling of the current message to the active dialog stack entry.
361
+ def route_to_active_dialog; end
362
+
363
+ private
364
+
365
+ def start_batch
366
+ @batch_started = true
367
+ unless @sending_batch
368
+ @batch_timer.cancel if @batch_timer
369
+ # TODO: send_batch after config[:auto_batch_delay] seconds
370
+ @batch_timer = @timers.after(config[:auto_batch_delay]) do
371
+ send_batch
372
+ end
373
+ end
374
+ end
375
+
376
+ def create_message; end
377
+
378
+ def prepare_message(msg)
379
+ msg[:type] ||= 'message'
380
+ msg[:address] ||= message[:address]
381
+ msg[:text_locale] ||= message[:text_locale]
382
+ msg
383
+ end
384
+
385
+ def vget_text; end
386
+
387
+ def validate_call_stack; end
388
+
389
+ def resolve_dialog_id; end
390
+
391
+ def cur_library_name; end
392
+
393
+ def find_dialog; end
394
+
395
+ def push_dialog; end
396
+
397
+ def pop_dialog; end
398
+
399
+ def delete_dialogs; end
400
+
401
+ def cur_dialog
402
+ ss = @session_sate
403
+ cur = ss.call_stack.last unless ss.call_stack.empty?
404
+ cur
405
+ end
406
+ end
407
+ end