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,6 @@
1
+ require 'pry'
2
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
3
+ require 'bot_framework'
4
+ require_relative 'bot'
5
+ # include BotFramework
6
+ run BotFramework::Server
Binary file
Binary file
@@ -2,6 +2,8 @@ require 'oauth2'
2
2
  require 'jwt'
3
3
  require 'httparty'
4
4
  require 'json'
5
+ require 'logger'
6
+
5
7
  require 'bot_framework/version'
6
8
  require 'bot_framework/errors'
7
9
  require 'bot_framework/util'
@@ -12,6 +14,7 @@ require 'bot_framework/bot_state'
12
14
  require 'bot_framework/token_validator'
13
15
  require 'bot_framework/bot'
14
16
  require 'bot_framework/server'
17
+ # Models
15
18
  require 'bot_framework/models/base'
16
19
  require 'bot_framework/models/activity'
17
20
  require 'bot_framework/models/api_response'
@@ -37,6 +40,11 @@ require 'bot_framework/models/resource_response'
37
40
  require 'bot_framework/models/signin_card'
38
41
  require 'bot_framework/models/thumbnail_card'
39
42
 
43
+ # Dialog
44
+ require 'bot_framework/dialogs/entity_recognizer'
45
+ require 'bot_framework/dialogs/luis_recognizer'
46
+ require 'bot_framework/dialogs/reg_exp_recognizer'
47
+
40
48
  module BotFramework
41
49
  class << self
42
50
  attr_accessor :connector
@@ -44,5 +52,9 @@ module BotFramework
44
52
  def configure(*args, &block)
45
53
  @connector = Connector.new(*args, &block)
46
54
  end
55
+
56
+ def logger
57
+ @logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT)
58
+ end
47
59
  end
48
60
  end
@@ -1,3 +1,5 @@
1
+ require 'uri'
2
+
1
3
  module BotFramework
2
4
  class ApiBase
3
5
  include HTTParty
@@ -8,23 +10,23 @@ module BotFramework
8
10
  end
9
11
 
10
12
  def api_get(local_uri, _opts = {})
11
- uri = service_url + local_uri
13
+ uri = URI.join(service_url, URI.escape(local_uri))
12
14
  JSON.parse(BotFramework.connector.token.get(uri).body)
13
15
  end
14
16
 
15
17
  def api_post(local_uri, opts = {})
16
- uri = service_url + local_uri
18
+ uri = URI.join(service_url, URI.escape(local_uri))
17
19
  JSON.parse(BotFramework.connector.token.post(uri, body: opts.to_json,
18
20
  headers: { 'Content-Type' => 'application/json' }).body)
19
21
  end
20
22
 
21
23
  def api_delete(local_uri)
22
- uri = service_url + local_uri
24
+ uri = URI.join(service_url, URI.escape(local_uri))
23
25
  BotFramework.connector.token.delete(uri)
24
26
  end
25
27
 
26
28
  def api_request(method, local_uri, opts)
27
- uri = service_url + local_uri
29
+ uri = URI.join(service_url, URI.escape(local_uri))
28
30
  BotFramework.connector.token.request(method, uri, opts)
29
31
  end
30
32
  end
@@ -1,17 +1,58 @@
1
1
  module BotFramework
2
2
  class Bot
3
3
  class << self
4
+ extend Gem::Deprecate
5
+ attr_accessor :recognizers
6
+
4
7
  def on(event, &block)
5
8
  hooks[event] = block
6
9
  end
7
10
 
11
+ def on_intent(intent, &block)
12
+ intent_callbacks[intent] = block
13
+ end
14
+
15
+ def recognizer=(recognizer)
16
+ warn "DEPRECATED: Use add_recognizer method instead"
17
+ add_recognizer(recognizer)
18
+ end
19
+ deprecate :recognizer=, :add_recognizer, 2016, 5
20
+
21
+ def add_recognizer(recognizer)
22
+ recognizers << recognizer
23
+ end
24
+
25
+ def recognizers
26
+ @recognizers ||= []
27
+ end
28
+
8
29
  def trigger(event, *args)
9
30
  # hooks.fetch(event).call(*args)
10
- instance_exec *args, &hooks.fetch(event)
31
+ if hooks[event].nil?
32
+ BotFramework.logger.info "No call back registered for #{event}"
33
+ return false
34
+ end
35
+ instance_exec(*args, &hooks.fetch(event))
36
+ end
37
+
38
+ def trigger_intent_call_back(intent, *args)
39
+ if intent_callbacks[intent].nil?
40
+ BotFramework.logger.info "No call back registered for #{intent}"
41
+ trigger_intent_call_back(:default, *args) if intent_callbacks[:default]
42
+ return false
43
+ end
44
+ instance_exec(*args, &intent_callbacks.fetch(intent))
11
45
  end
12
46
 
13
47
  def receive(payload)
48
+ trigger(payload.type.to_sym)
49
+ # Run on default
14
50
  trigger(:activity, payload)
51
+ recognizers.each do |recognizer|
52
+ recognizer.recognize(message: payload.as_json) do |_error, intents|
53
+ trigger_intent_call_back(intents[:intent], payload, intents) if intents[:intent]
54
+ end
55
+ end
15
56
  end
16
57
 
17
58
  def reply(activity, message = '')
@@ -19,7 +60,7 @@ module BotFramework
19
60
  end
20
61
 
21
62
  def user_data=(data)
22
- p "Data set as #{data}"
63
+ BotFramework.logger.info "Data set as #{data}"
23
64
  end
24
65
 
25
66
  def set_conversation_data(activity, data)
@@ -43,6 +84,10 @@ module BotFramework
43
84
  def reset_hooks
44
85
  @hooks = {}
45
86
  end
87
+
88
+ def intent_callbacks
89
+ @intent_callbacks ||= {}
90
+ end
46
91
  end
47
92
  end
48
93
  end
@@ -4,6 +4,16 @@ module BotFramework
4
4
  # include HTTParty
5
5
  attr_accessor :app_id, :app_secret, :token
6
6
  CONFIG_URI = 'https://api.aps.skype.com/v1/.well-known/openidconfiguration'.freeze
7
+ REFRESH_ENDPOINT = 'https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token'.freeze
8
+ REFRESH_SCOPE = 'https://api.botframework.com/.default'.freeze
9
+ OPEN_ID_METADATA = 'https://login.botframework.com/v1/.well-known/openidconfiguration'.freeze
10
+ BOT_CONNECTOR_ISSUER = 'https://api.botframework.com'.freeze
11
+ MSA_OPEN_ID_METADATA = 'https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration'.freeze
12
+ MSA_ISSUER = 'https://sts.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47/'.freeze
13
+ MSA_AUDIENCE = 'https://graph.microsoft.com'.freeze
14
+ EMULATOR_AUDIENCE_METADATA = 'https://login.microsoftonline.com/botframework.com/v2.0/.well-known/openid-configuration'.freeze
15
+ EMULATOR_AUDIENCE = 'https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/'.freeze
16
+ STATE_END_POINT = 'https://state'.freeze
7
17
 
8
18
  def initialize(options = {})
9
19
  @app_id = options[:app_id]
@@ -13,8 +23,8 @@ module BotFramework
13
23
 
14
24
  def client
15
25
  OAuth2::Client.new(app_id, app_secret,
16
- authorize_url: '/common/oauth2/v2.0/authorize',
17
- token_url: '/common/oauth2/v2.0/token',
26
+ authorize_url: 'botframework.com/oauth2/v2.0/authorize',
27
+ token_url: 'botframework.com/oauth2/v2.0/token',
18
28
  raise_errors: true,
19
29
  site: 'https://login.microsoftonline.com')
20
30
  end
@@ -26,9 +36,7 @@ module BotFramework
26
36
  end
27
37
 
28
38
  def get_token
29
- client.client_credentials.get_token(scope: 'https://graph.microsoft.com/.default',
30
- client_id: app_id,
31
- client_secret: app_secret)
39
+ client.client_credentials.get_token(scope: 'https://api.botframework.com/.default', token_method: :post)
32
40
  end
33
41
  end
34
42
  end
@@ -0,0 +1,17 @@
1
+ require 'observer'
2
+ require 'nio'
3
+ module BotFramework
4
+ class ConsoleConnector
5
+ include Observable
6
+ def listen
7
+ BotFramework.logger.info 'Listening'
8
+ loop do
9
+ line = Readline.readline('> ')
10
+ break if line.nil? || line == 'quit'
11
+ Readline::HISTORY.push(line)
12
+ BotFramework.logger.info "You typed: #{line}"
13
+ end
14
+ self
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,66 @@
1
+ module BotFramework
2
+ module Dialogs
3
+ class ActionSet
4
+ attr_accessor :actions
5
+
6
+ def initialize
7
+ @actions = {}
8
+ end
9
+
10
+ def clone(copy_to = nil)
11
+ obj = copy_to || ActionSet.new
12
+ obj.trigger = trigger
13
+ actions.each do |name|
14
+ object.actions[name] = actions[name]
15
+ end
16
+ obj
17
+ end
18
+
19
+ def add_dialog_trigger(actions, dialog_id)
20
+ if trigger
21
+ trigger.localization_namespace = dialog_id.split(':').first
22
+ actions.begin_dialog_action(dialog_id, dialog_id, trigger)
23
+ end
24
+ end
25
+
26
+ def find_action_routes(context, callback); end
27
+
28
+ def select_action_route(session, route); end
29
+
30
+ def dialog_interrupted(session, dialog_id, dialog_ags); end
31
+
32
+ def begin_dialog_action(name, id, options = {}); end
33
+
34
+ def end_conversation_action(name, message, options); end
35
+
36
+ def reload_action(name, message, options); end
37
+
38
+ def cancel_action(name, message, options)
39
+ action(name, options) do |args, session|
40
+ if options[:confirm_prompt]
41
+ session.begin_dialog('BotBuilder:ConfirmCancel', localization_namespace: nil,
42
+ confirm_prompt: nil,
43
+ dialog_index: nil,
44
+ msg: message)
45
+ elsif message.present?
46
+ session.send_localized(args.library_name, message)
47
+ else
48
+ session.cancel_dialog(args.dialog_index)
49
+ end
50
+ end
51
+ end
52
+
53
+ def select_action_routes; end
54
+
55
+ def trigger_action; end
56
+
57
+ private
58
+
59
+ def action(_name, _options = {})
60
+ yield
61
+ end
62
+
63
+ def recognize_action(message); end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,25 @@
1
+ require_relative '../session'
2
+ require_relative 'action_set'
3
+ module BotFramework
4
+ # Abstract class for dialog
5
+ module Dialogs
6
+ class Dialog < ActionSet
7
+ RESUME_REASONS = [:completed, :not_completed, :canceled, :back, :forward, :reprompt].freeze
8
+ def begin(session, _opts = {})
9
+ reply_recieved(session)
10
+ end
11
+
12
+ def reply_recieved(_session, _recognize_result = {})
13
+ raise NotImplementedError
14
+ end
15
+
16
+ def dialog_resumed(session, result)
17
+ session.error(result[:error]) if result[:error]
18
+ end
19
+
20
+ def recognize(_context)
21
+ yield nil, { score: 0.1 }
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,123 @@
1
+ require 'chronic'
2
+ module BotFramework
3
+ module Dialogs
4
+ class EntityRecognizer
5
+ DATE_REGEX = /^\d{4}-\d{2}-\d{2}/i
6
+ YES_REGEX = /^(1|y|yes|yep|sure|ok|true)(\W|$)/i
7
+ NO_REGX = /^(2|n|no|nope|not|false)(\W|$)/i
8
+ NUMBER_REGEX = /[+-]?(?:\d+\.?\d*|\d*\.?\d+)/
9
+ ORDINAL_WORDS_REGEX = /first|second|third|fourth|fifth|sixth|seventh|eigth|ninth|tenth/
10
+ def initialize(*args); end
11
+
12
+ def self.find_entity(entities, type)
13
+ # raise ArgumentError unless entities.is_a? Hash
14
+ entities.find { |entity| entity[:type] == type }
15
+ end
16
+
17
+ def self.find_all_entities(entities, type)
18
+ entities.find_all { |entity| entity[:type] == type }
19
+ end
20
+
21
+ def self.parse_time(entities)
22
+ entities = [EntityRecognizer.recognize_time(entities)] if entities.is_a? String
23
+ EntityRecognizer.resolve_time(entities)
24
+ end
25
+
26
+ def self.resolve_time(entities)
27
+ now = DateTime.now
28
+ resolved_date = nil
29
+ time = nil
30
+ entities.each do |entity|
31
+ next unless entity[:resolution]
32
+ case entity[:resolution][:resolution_type] || entity[:type]
33
+ when 'builtin.datetime'
34
+ when 'builtin.datetime.date'
35
+ when 'builtin.datetime.time'
36
+ time = entity[:resolution][:time]
37
+ when 'chronic.time'
38
+ duration = entity
39
+ time = entity[:resolution][:time]
40
+ resolved_date = duration[:resolution][:time]
41
+ end
42
+ end
43
+ date = now if !resolved_date && (date || time)
44
+ time
45
+ end
46
+
47
+ def self.recognize_time(utterance, reference_date = {})
48
+ time = Chronic.parse(utterance, reference_date)
49
+ return false unless time
50
+ {
51
+ type: 'chronic.time',
52
+ entity: time.to_s,
53
+ resolution: {
54
+ resolution_type: 'chronic.time',
55
+ time: time
56
+ }
57
+ }
58
+ end
59
+
60
+ def self.parse_number(entities)
61
+ entity = nil
62
+ entity = if entities.is_a? String
63
+ { type: 'text', entity: entities.strip }
64
+ else
65
+ find_entity(entities, 'builtin.number')
66
+ end
67
+
68
+ if entity
69
+ match = NUMBER_REGEX.match(entity[:entity])
70
+ return match[0] if match
71
+ end
72
+ end
73
+
74
+ def self.parse_boolean(utterance)
75
+ utterance.strip!
76
+ if YES_REGEX =~ utterance
77
+ true
78
+ elsif NO_REGX =~ utterance
79
+ false
80
+ end
81
+ end
82
+
83
+ def self.find_best_match(choices, utterance, threshhold = 0.6)
84
+ matches = find_all_matches(choices, utterance, threshhold)
85
+ matches.max { |entry| entry[:score] }
86
+ end
87
+
88
+ def self.find_all_matches(choices, utterance, threshold = 0.6)
89
+ matches = []
90
+ utterance = utterance.strip.downcase
91
+ tokens = utterance.split
92
+
93
+ expand_choices(choices).each_with_index do |choice, index|
94
+ score = 0.0
95
+ value = choice.strip.downcase
96
+ if value.include?(utterance)
97
+ score = utterance.size.to_f / value.size
98
+ elsif utterance.include? value
99
+ score = [0.5 + (value.size.to_f / utterance.size), 0.9].min
100
+ else
101
+ matched = ''
102
+ tokens.each { |token| matched += token if value.include? token }
103
+ score = matched.size / value.size
104
+ end
105
+ if score > threshold
106
+ matches.push(index: index, entity: choice, score: score)
107
+ end
108
+ end
109
+ matches
110
+ end
111
+
112
+ def self.expand_choices(choices)
113
+ case choices
114
+ when nil then []
115
+ when Array then choices.map(&:to_s)
116
+ when Hash then choices.keys.map(&:to_s)
117
+ when String then choices.split('|')
118
+ else [choices.to_s]
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,72 @@
1
+ module BotFramework
2
+ module Dialogs
3
+ class LuisRecognizer
4
+ def initialize(models)
5
+ @models = { '*' => models }
6
+ end
7
+
8
+ def recognize(context)
9
+ result = { score: 0.0, intent: nil }
10
+ if context && context[:message] && context[:message][:text]
11
+ utterance = context[:message][:text]
12
+ locale = context[:locale] || '*'
13
+ model = @models[locale] || @models['*']
14
+ if model
15
+ LuisRecognizer.recognize(utterance, model) do |error, intents, entities|
16
+ if error
17
+ yield 'Error', nil
18
+ else
19
+ result[:intents] = intents
20
+ result[:entities] = entities
21
+
22
+ top = intents.max { |intent| intent[score] }
23
+ if top
24
+ result[:score] = top[:score]
25
+ result[:intent] = top[:intent]
26
+ case top[:intent].downcase
27
+ when 'builtin.intent.none'
28
+ when 'none'
29
+ result[:score] = 0.1
30
+ end
31
+ end
32
+ yield nil, result
33
+ end
34
+ yield nil, result
35
+ end
36
+ else
37
+ yield StandardError.new("Luis model not found for locale #{locale}"), nil
38
+ end
39
+ else
40
+ yield nil, result
41
+ end
42
+ end
43
+
44
+ def self.recognize(utterance, model_uri)
45
+ uri = model_uri.strip
46
+ uri += '&q=' unless uri.end_with? '&q='
47
+ uri += URI.encode_www_form_component(utterance || '')
48
+ result = JSON.parse HTTParty.get(uri).body
49
+ # Symbolize keys
50
+ result = result.each_with_object({}) { |(k, v), temp| temp[k.to_sym] = v; temp }
51
+ result[:intents] ||= []
52
+ result[:entities] ||= []
53
+ if result[:topScoringIntent] && result[:intents].empty?
54
+ result[:intents] << result[:topScoringIntent]
55
+ end
56
+
57
+ if result[:intents].length == 1 && (result[:intents].first[:score].is_a? Numeric)
58
+ result[:intents].first[:score] = 1.0
59
+ end
60
+ # Symbolize keys
61
+ result[:intents].map! do |intent|
62
+ intent.each_with_object({}) { |(k, v), temp| temp[k.to_sym] = v; temp }
63
+ end
64
+
65
+ result[:entities].map! do |entity|
66
+ entity.each_with_object({}) { |(k, v), temp| temp[k.to_sym] = v; temp }
67
+ end
68
+ yield nil, result[:intents], result[:entities]
69
+ end
70
+ end
71
+ end
72
+ end