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.
- checksums.yaml +4 -4
- data/.travis.yml +4 -1
- data/README.md +14 -1
- data/appveyor.yml +20 -0
- data/bot_framework.gemspec +6 -0
- data/examples/dialog/basic_first_run.rb +12 -0
- data/examples/ruby_conf/Gemfile +8 -0
- data/examples/ruby_conf/Gemfile.lock +85 -0
- data/examples/ruby_conf/bot.rb +88 -0
- data/examples/ruby_conf/config.ru +6 -0
- data/examples/ruby_conf/speaker_data.yaml +41 -0
- data/examples/simple_regex/Gemfile +6 -0
- data/examples/simple_regex/Gemfile.lock +63 -0
- data/examples/simple_regex/bot.rb +35 -0
- data/examples/simple_regex/config.ru +6 -0
- data/images/emulator1.png +0 -0
- data/images/emulator2.png +0 -0
- data/lib/bot_framework.rb +12 -0
- data/lib/bot_framework/api_base.rb +6 -4
- data/lib/bot_framework/bot.rb +47 -2
- data/lib/bot_framework/connector.rb +13 -5
- data/lib/bot_framework/console_connector.rb +17 -0
- data/lib/bot_framework/dialogs/action_set.rb +66 -0
- data/lib/bot_framework/dialogs/dialog.rb +25 -0
- data/lib/bot_framework/dialogs/entity_recognizer.rb +123 -0
- data/lib/bot_framework/dialogs/luis_recognizer.rb +72 -0
- data/lib/bot_framework/dialogs/reg_exp_recognizer.rb +41 -0
- data/lib/bot_framework/dialogs/simple_dialog.rb +29 -0
- data/lib/bot_framework/events/event_emitter.rb +6 -0
- data/lib/bot_framework/message.rb +80 -0
- data/lib/bot_framework/models/object.rb +0 -1
- data/lib/bot_framework/prompt.rb +62 -0
- data/lib/bot_framework/server.rb +1 -1
- data/lib/bot_framework/session.rb +407 -0
- data/lib/bot_framework/simple_prompt_recognizer.rb +4 -0
- data/lib/bot_framework/token_validator.rb +6 -4
- data/lib/bot_framework/universal_bot.rb +7 -0
- data/lib/bot_framework/version.rb +1 -1
- metadata +100 -4
Binary file
|
Binary file
|
data/lib/bot_framework.rb
CHANGED
@@ -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
|
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
|
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
|
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
|
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
|
data/lib/bot_framework/bot.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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: '/
|
17
|
-
token_url: '/
|
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://
|
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
|