aws-lex-conversation 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 148e207d8dcce051f2fc01244390021ae5734037a3b856462cd76ccd9e4cde8a
4
- data.tar.gz: 7f019bdc51b68e037575a60784dbe76217aabccdf15e3394515d4a404dbdd85a
3
+ metadata.gz: 3d7e1994eb1948eceb9d236d65c0cc449e5120b6c91fb86585a7225e660cbc84
4
+ data.tar.gz: 071c8e344b1c6d78597986f679a7446250d2d2d75a001d4af7d393043c567529
5
5
  SHA512:
6
- metadata.gz: 856f7ab5e25ffc87f93f3ee6f2b834acdd47cbc28adf334a22af4b152b37de435f54d3715374f33acfdf4ceebca1e2945b3557b5f72c4c5873e4b8761af5758a
7
- data.tar.gz: e6dff0de003696ef87460e6dfdb54b94e470d4ff5b6470f43642dd259fa13b645b276d5d88413369dcacc7368004f4c93d92f38c1c50a5a18487c6cb2210e62d
6
+ metadata.gz: c83c69dfb06fd6faa16af4dfeb37119b84054c473e11929f201b34b419aba071918405635587ad38b21ce6d97862f60f1e360d9867059942deaea348a9eab176
7
+ data.tar.gz: f3cb8c0162884104b0ccb6f921c3cacdd08412d83b32e2463178b0a945b8e795de8da9bb99eabaaaf814de3b19b930e0a59972aea0dd128d8ca29d0da7e05982
@@ -9,6 +9,14 @@ Documentation:
9
9
  Enabled: false
10
10
  Gemspec/RequiredRubyVersion:
11
11
  Enabled: false
12
+ Layout/FirstArrayElementIndentation:
13
+ Enabled: false
14
+ Layout/MultilineMethodCallIndentation:
15
+ Enabled: false
16
+ Layout/SpaceAroundOperators:
17
+ Enabled: false
18
+ Metrics/AbcSize:
19
+ Max: 18
12
20
  Metrics/ClassLength:
13
21
  Max: 150
14
22
  Metrics/BlockLength:
@@ -27,3 +35,5 @@ Style/Lambda:
27
35
  EnforcedStyle: literal
28
36
  Style/GuardClause:
29
37
  Enabled: false
38
+ Style/SlicingWithRange:
39
+ Enabled: false
@@ -0,0 +1,24 @@
1
+ # 2.0.0 - August 19, 2020
2
+
3
+ * **breaking change:** Rename `Aws::Lex::Conversation::Type::CurrentIntent` to `Aws::Lex::Conversation::Type::Intent`.
4
+ * **breaking change:** Built-in handlers now default the `options` attribute to an empty hash.
5
+ * Add Lex NLU model improvement functionality (see: https://aws.amazon.com/about-aws/whats-new/2020/08/amazon-lex-launches-accuracy-improvements-and-confidence-scores/).
6
+ * Add the `intent_confidence` method to the conversation class that may be used as follows:
7
+
8
+ ```ruby
9
+ # NOTE: Lex model improvements must be enabled on the bot to get confidence data.
10
+ # SEE: https://aws.amazon.com/about-aws/whats-new/2020/08/amazon-lex-launches-accuracy-improvements-and-confidence-scores/
11
+ conversation.intent_confidence.ambiguous? # true/false
12
+ conversation.intent_confidence.unambiguous? # true/false
13
+ conversation.intent_confidence.candidates # [...] the array contains the current_intent and all similar intents
14
+ conversation.intent_confidence.similar_alternates # [...] the array doesn't contain the current_intent
15
+ ```
16
+
17
+ * The calculation used to determine intent ambiguity by default looks for confidence scores that are within a standard deviation of the current intent's confidence score.
18
+ * You can pass your own static `threshold` parameter if you wish to change this behaviour:
19
+
20
+ ```ruby
21
+ conversation.intent_confidence.ambiguous?(threshold: 0.4) # true/false
22
+ ```
23
+
24
+ * Implement a built-in `SlotResolution` handler that is intended to act as the initial handler in the chain. This handler will resolve all slot values to their top resolution, then call the successor handler.
data/README.md CHANGED
@@ -58,13 +58,13 @@ The first handler that returns `true` for the `will_respond?` method will provid
58
58
  ```ruby
59
59
  class SayHello < Aws::Lex::Conversation::Handler::Base
60
60
  def will_respond?(conversation)
61
- conversation.lex.incovation_source.dialog_code_hook? && # callback is for DialogCodeHook (i.e. validation)
61
+ conversation.lex.invocation_source.dialog_code_hook? && # callback is for DialogCodeHook (i.e. validation)
62
62
  conversation.lex.current_intent.name == 'SayHello' && # Lex has routed to the 'SayHello' intent
63
- conversation.slots[:name] # our expected slot value is set
63
+ conversation.slots[:name].filled? # our expected slot value is set
64
64
  end
65
65
 
66
66
  def response(conversation)
67
- name = conversation.slots[:name]
67
+ name = conversation.slots[:name].value
68
68
 
69
69
  # NOTE: you can use the Type::* classes if you wish. The final output
70
70
  # will be normalized to a value that complies with the Lex response format.
@@ -141,6 +141,34 @@ conversation.handlers = [
141
141
  conversation.respond # => { dialogAction: { type: 'Delegate' } }
142
142
  ```
143
143
 
144
+ ### `Aws::Lex::Conversation::Handler::SlotResolution`
145
+
146
+ This handler will set all slot values equal to their top resolution in the input event. The handler then calls the next handler in the chain for a response.
147
+
148
+ **NOTE:** This handler must not be the final handler in the chain. An exception of type `Aws::Lex::Conversation::Exception::MissingHandler` will be raised if there is no successor handler.
149
+
150
+ | Option | Required | Description | Default Value |
151
+ |------------------|----------|--------------------------------------------------------------|-------------------------------------|
152
+ | respond_on | No | A callable that provides the condition for `will_handle?`. | `->(c) { true }` |
153
+
154
+ i.e.
155
+
156
+ ```ruby
157
+ conversation = Aws::Lex::Conversation.new(event: event, context: context)
158
+ conversation.handlers = [
159
+ {
160
+ handler: Aws::Lex::Conversation::Handler::SlotResolution
161
+ },
162
+ {
163
+ handler: Aws::Lex::Conversation::Handler::Delegate,
164
+ options: {
165
+ respond_on: ->(c) { true }
166
+ }
167
+ }
168
+ ]
169
+ conversation.respond # => { dialogAction: { type: 'Delegate' } }
170
+ ```
171
+
144
172
  ## Development
145
173
 
146
174
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -42,6 +42,10 @@ module Aws
42
42
  chain.first.handle(self)
43
43
  end
44
44
 
45
+ def intent_confidence
46
+ @intent_confidence ||= Type::IntentConfidence.new(event: lex)
47
+ end
48
+
45
49
  def intent_name
46
50
  lex.current_intent.name
47
51
  end
@@ -4,6 +4,7 @@ require 'json'
4
4
  require 'shrink/wrap'
5
5
 
6
6
  require_relative 'version'
7
+ require_relative 'exception/missing_handler'
7
8
  require_relative 'response/base'
8
9
  require_relative 'response/close'
9
10
  require_relative 'response/confirm_intent'
@@ -24,11 +25,12 @@ require_relative 'type/invocation_source'
24
25
  require_relative 'type/dialog_action_type'
25
26
  require_relative 'type/confirmation_status'
26
27
  require_relative 'type/fulfillment_state'
28
+ require_relative 'type/intent_confidence'
27
29
  require_relative 'type/recent_intent_summary_view'
28
30
  require_relative 'type/slot'
29
31
  require_relative 'type/slot_resolution'
30
32
  require_relative 'type/slot_detail'
31
- require_relative 'type/current_intent'
33
+ require_relative 'type/intent'
32
34
  require_relative 'type/output_dialog_mode'
33
35
  require_relative 'type/bot'
34
36
  require_relative 'type/message/content_type'
@@ -42,3 +44,4 @@ require_relative 'type/event'
42
44
  require_relative 'handler/base'
43
45
  require_relative 'handler/echo'
44
46
  require_relative 'handler/delegate'
47
+ require_relative 'handler/slot_resolution'
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aws
4
+ module Lex
5
+ class Conversation
6
+ module Exception
7
+ class MissingHandler < StandardError
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -9,7 +9,7 @@ module Aws
9
9
 
10
10
  def initialize(opts = {})
11
11
  self.successor = opts[:successor]
12
- self.options = opts[:options]
12
+ self.options = opts[:options] || {}
13
13
  end
14
14
 
15
15
  def will_respond?(conversation)
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aws
4
+ module Lex
5
+ class Conversation
6
+ module Handler
7
+ class SlotResolution < Base
8
+ def will_respond?(conversation)
9
+ # respond by default unless told otherwise
10
+ callable = options.fetch(:respond_on) { ->(_c) { true } }
11
+ callable.call(conversation)
12
+ end
13
+
14
+ def response(conversation)
15
+ # resolve all slots to their top resolution
16
+ conversation.slots.values.each(&:resolve!)
17
+
18
+ unless successor
19
+ msg = 'Handler `SlotResolution` must not be the final handler in the chain'
20
+ raise Exception::MissingHandler, msg
21
+ end
22
+
23
+ # call the next handler in the chain
24
+ successor.handle(conversation)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -45,7 +45,11 @@ module Aws
45
45
  end
46
46
 
47
47
  def elicitation_content
48
- first_elicitation? ? message : follow_up_message
48
+ first_elicitation? ? compose_message(message) : compose_message(follow_up_message)
49
+ end
50
+
51
+ def compose_message(msg)
52
+ msg.is_a?(Proc) ? msg.call(conversation) : msg
49
53
  end
50
54
 
51
55
  def increment_slot_elicitations!
@@ -19,7 +19,8 @@ module Aws
19
19
 
20
20
  module InstanceMethods
21
21
  def assign_attributes!(opts = {})
22
- self.class.attributes.each do |attribute|
22
+ attributes = self.class.attributes | self.class.virtual_attributes
23
+ attributes.each do |attribute|
23
24
  instance_variable_set("@#{attribute}", opts[attribute])
24
25
  end
25
26
  end
@@ -52,14 +53,29 @@ module Aws
52
53
  end
53
54
 
54
55
  module ClassMethods
55
- def integer!
56
- ->(v) { v.to_i }
56
+ def integer!(nilable: false)
57
+ nilable ? ->(v) { v&.to_i } : ->(v) { v.to_i }
58
+ end
59
+
60
+ def float!(nilable: false)
61
+ nilable ? ->(v) { v&.to_f } : ->(v) { v.to_f }
57
62
  end
58
63
 
59
64
  def symbolize_hash!
60
65
  ->(v) { v.transform_keys(&:to_sym) }
61
66
  end
62
67
 
68
+ def computed_property(attribute, callable)
69
+ mapping[attribute] = attribute
70
+ attr_writer(attribute)
71
+
72
+ # dynamically memoize the result
73
+ define_method(attribute) do
74
+ instance_variable_get("@#{attribute}") ||
75
+ instance_variable_set("@#{attribute}", callable.call(self))
76
+ end
77
+ end
78
+
63
79
  def required(attribute, opts = {})
64
80
  property(attribute, opts.merge(allow_nil: false))
65
81
  end
@@ -76,7 +92,12 @@ module Aws
76
92
 
77
93
  attr_accessor(attribute)
78
94
 
79
- mapping[attribute] = from
95
+ if opts.fetch(:virtual) { false }
96
+ virtual_attributes << attribute
97
+ else
98
+ mapping[attribute] = from
99
+ end
100
+
80
101
  translate(attribute => params)
81
102
  end
82
103
 
@@ -84,6 +105,10 @@ module Aws
84
105
  @attributes ||= mapping.keys
85
106
  end
86
107
 
108
+ def virtual_attributes
109
+ @virtual_attributes ||= []
110
+ end
111
+
87
112
  def mapping
88
113
  @mapping ||= {}
89
114
  end
@@ -7,6 +7,7 @@ module Aws
7
7
  class Event
8
8
  include Base
9
9
 
10
+ required :alternative_intents, default: -> { [] }
10
11
  required :current_intent
11
12
  required :bot
12
13
  required :user_id
@@ -20,8 +21,13 @@ module Aws
20
21
  optional :sentiment_response
21
22
  optional :kendra_response
22
23
 
24
+ computed_property :intents, ->(instance) do
25
+ [instance.current_intent] | instance.alternative_intents
26
+ end
27
+
23
28
  coerce(
24
- current_intent: CurrentIntent,
29
+ alternative_intents: Array[Intent],
30
+ current_intent: Intent,
25
31
  bot: Bot,
26
32
  invocation_source: InvocationSource,
27
33
  output_dialog_mode: OutputDialogMode,
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aws
4
+ module Lex
5
+ class Conversation
6
+ module Type
7
+ class Intent
8
+ include Base
9
+
10
+ required :name
11
+ required :raw_slots, from: :slots, virtual: true
12
+ required :slot_details
13
+ required :confirmation_status
14
+ optional :nlu_intent_confidence_score
15
+
16
+ computed_property :slots, ->(instance) do
17
+ instance.raw_slots.each_with_object({}) do |(key, value), hash|
18
+ hash[key.to_sym] = Slot.shrink_wrap(
19
+ name: key,
20
+ value: value,
21
+ # pass a reference to the parent down to the slot so that each slot
22
+ # instance can view a broader scope such as slot_details/resolutions
23
+ current_intent: instance
24
+ )
25
+ end
26
+ end
27
+
28
+ class << self
29
+ def slot_details!
30
+ ->(val) do
31
+ val
32
+ .reject { |_, v| v.nil? }
33
+ .each_with_object({}) do |(key, value), hash|
34
+ hash[key.to_sym] = SlotDetail.shrink_wrap(value)
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ coerce(
41
+ slot_details: slot_details!,
42
+ confirmation_status: ConfirmationStatus,
43
+ nlu_intent_confidence_score: float!(nilable: true)
44
+ )
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aws
4
+ module Lex
5
+ class Conversation
6
+ module Type
7
+ class IntentConfidence
8
+ include Base
9
+
10
+ required :event
11
+
12
+ def ambiguous?(threshold: standard_deviation)
13
+ candidates(threshold: threshold).size > 1
14
+ end
15
+
16
+ def unambiguous?(threshold: standard_deviation)
17
+ !ambiguous?(threshold: threshold)
18
+ end
19
+
20
+ # NOTE: by default this method looks for candidates
21
+ # with a confidence score within one standard deviation
22
+ # of the current intent. Confidence scores may not be
23
+ # normally distributed, so it's very possible that this
24
+ # method will return abnormal results for skewed sample sets.
25
+ #
26
+ # If you want a consistent threshold for the condition, pass
27
+ # a static `threshold` parameter.
28
+ def candidates(threshold: standard_deviation)
29
+ intents = event.intents.select do |intent|
30
+ diff = event.current_intent
31
+ .nlu_intent_confidence_score
32
+ .to_f
33
+ .-(intent.nlu_intent_confidence_score.to_f)
34
+ .abs
35
+
36
+ diff <= threshold
37
+ end
38
+
39
+ # sort descending
40
+ intents.sort do |a, b|
41
+ b.nlu_intent_confidence_score.to_f <=> a.nlu_intent_confidence_score.to_f
42
+ end
43
+ end
44
+
45
+ def mean
46
+ @mean ||= calculate_mean
47
+ end
48
+
49
+ def similar_alternates(threshold: standard_deviation)
50
+ # remove the first element (current intent) from consideration
51
+ candidates(threshold: threshold)[1..-1]
52
+ end
53
+
54
+ def standard_deviation
55
+ @standard_deviation ||= calculate_standard_deviation
56
+ end
57
+
58
+ private
59
+
60
+ def calculate_mean
61
+ sum = event.intents.sum { |i| i.nlu_intent_confidence_score.to_f }
62
+ sum / event.intents.size
63
+ end
64
+
65
+ def calculate_standard_deviation
66
+ normalized = event.intents.map do |intent|
67
+ (intent.nlu_intent_confidence_score.to_f - mean) ** 2
68
+ end
69
+ normalized_mean = normalized.sum / normalized.size
70
+ Math.sqrt(normalized_mean)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -7,9 +7,14 @@ module Aws
7
7
  class Slot
8
8
  include Base
9
9
 
10
+ required :current_intent, from: :current_intent, virtual: true
10
11
  required :name
11
12
  required :value
12
13
 
14
+ def as_json(_opts = {})
15
+ to_lex
16
+ end
17
+
13
18
  def to_lex
14
19
  value
15
20
  end
@@ -17,6 +22,28 @@ module Aws
17
22
  def filled?
18
23
  value.to_s != ''
19
24
  end
25
+
26
+ def resolve!(index: 0)
27
+ self.value = resolved(index: index)
28
+ end
29
+
30
+ def resolved(index: 0)
31
+ details.resolutions.fetch(index) { SlotResolution.new(value: value) }.value
32
+ end
33
+
34
+ def original_value
35
+ details.original_value
36
+ end
37
+
38
+ def resolvable?
39
+ details.resolutions.any?
40
+ end
41
+
42
+ def details
43
+ @details ||= current_intent.slot_details.fetch(name.to_sym) do
44
+ SlotDetail.new(name: name, resolutions: [], original_value: value)
45
+ end
46
+ end
20
47
  end
21
48
  end
22
49
  end
@@ -3,7 +3,7 @@
3
3
  module Aws
4
4
  module Lex
5
5
  class Conversation
6
- VERSION = '1.0.0'
6
+ VERSION = '2.0.0'
7
7
  end
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aws-lex-conversation
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jesse Doyle
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: exe
14
14
  cert_chain: []
15
- date: 2020-06-26 00:00:00.000000000 Z
15
+ date: 2020-08-21 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: shrink_wrap
@@ -44,6 +44,7 @@ files:
44
44
  - ".rspec"
45
45
  - ".rubocop.yml"
46
46
  - ".simplecov"
47
+ - CHANGELOG.md
47
48
  - CODE_OF_CONDUCT.md
48
49
  - Gemfile
49
50
  - LICENSE.md
@@ -55,9 +56,11 @@ files:
55
56
  - lib/aws-lex-conversation.rb
56
57
  - lib/aws/lex/conversation.rb
57
58
  - lib/aws/lex/conversation/base.rb
59
+ - lib/aws/lex/conversation/exception/missing_handler.rb
58
60
  - lib/aws/lex/conversation/handler/base.rb
59
61
  - lib/aws/lex/conversation/handler/delegate.rb
60
62
  - lib/aws/lex/conversation/handler/echo.rb
63
+ - lib/aws/lex/conversation/handler/slot_resolution.rb
61
64
  - lib/aws/lex/conversation/response/base.rb
62
65
  - lib/aws/lex/conversation/response/close.rb
63
66
  - lib/aws/lex/conversation/response/confirm_intent.rb
@@ -72,11 +75,12 @@ files:
72
75
  - lib/aws/lex/conversation/type/base.rb
73
76
  - lib/aws/lex/conversation/type/bot.rb
74
77
  - lib/aws/lex/conversation/type/confirmation_status.rb
75
- - lib/aws/lex/conversation/type/current_intent.rb
76
78
  - lib/aws/lex/conversation/type/dialog_action_type.rb
77
79
  - lib/aws/lex/conversation/type/enumeration.rb
78
80
  - lib/aws/lex/conversation/type/event.rb
79
81
  - lib/aws/lex/conversation/type/fulfillment_state.rb
82
+ - lib/aws/lex/conversation/type/intent.rb
83
+ - lib/aws/lex/conversation/type/intent_confidence.rb
80
84
  - lib/aws/lex/conversation/type/invocation_source.rb
81
85
  - lib/aws/lex/conversation/type/message.rb
82
86
  - lib/aws/lex/conversation/type/message/content_type.rb
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Aws
4
- module Lex
5
- class Conversation
6
- module Type
7
- class CurrentIntent
8
- include Base
9
-
10
- required :name
11
- required :slots
12
- required :slot_details
13
- required :confirmation_status
14
-
15
- class << self
16
- def slots!
17
- ->(v) do
18
- v.each_with_object({}) do |(key, value), hash|
19
- hash[key.to_sym] = Slot.shrink_wrap(
20
- name: key,
21
- value: value
22
- )
23
- end
24
- end
25
- end
26
-
27
- def slot_details!
28
- ->(v) do
29
- v.each_with_object({}) do |(key, value), hash|
30
- hash[key.to_sym] = SlotDetail.shrink_wrap(value)
31
- end
32
- end
33
- end
34
- end
35
-
36
- coerce(
37
- slots: slots!,
38
- slot_details: slot_details!,
39
- confirmation_status: ConfirmationStatus
40
- )
41
- end
42
- end
43
- end
44
- end
45
- end